CALL

01What is it?
Log sales activities, calls, notes, meetings, tasks, against contacts and deals, with the mandatory create-then-associate step that makes them visible in the CRM. The value is a focused slice of go-to-market work judgment, useful when several similar skills cover the same ground.
02Inputs
Context for go-to-market work: your goals, audience, constraints, and any source material the skill asks for.
03Output
A ready-to-use result for go-to-market work: the analysis, copy, or recommendations the agent produces.
Install-only

Install as a package

Installs this one skill package for your coding agent, including any supporting files that skill ships with — not every skill in the repository. Read the tutorial.

Terminal
$ npx skills add hubspot/agent-cli-skills --skill sales-execution

Skill instructions

The instruction file for this skill. The skill also includes other files you need to install to use it.

SKILL.md

Resources

FileWhen to use
resources/activity-properties-reference.mdProperty names and enum values for calls/notes/meetings/tasks. Keep open while writing objects create — enum values are not discoverable via hubspot properties get today.

Read bulk-operations/SKILL.md first — this skill assumes its batching, pipe, and dry-run patterns.

The two non-obvious rules

1. Activities are invisible until associated. hubspot objects create --type calls ... alone produces a record nobody can see in the CRM UI. Always follow with hubspot associations create --from calls:<id> --to contacts:<id> (and the deal, if relevant) before stopping.

2. Timestamps differ between write and read.

PathFieldFormat
objects create --property hs_timestamp=...hs_timestampUnix ms (13 digits)
objects get --type calls <id> returnsproperties.hs_timestampUnix ms (string)
activities list --contact <id> returnstimestamp (flat, top-level)ISO 8601 (e.g. 2024-01-15T10:00:00Z)

Current Unix ms: $(date +%s)000 (macOS) or $(date +%s%3N) (Linux). activities list rows are {"id","type","timestamp","title","body","status","owner_id"} — the cross-type timeline read shape, no raw property names.

Create + associate, by type

# CALL
call_id=$(hubspot objects create --type calls \
  --property hs_call_title="Discovery call" \
  --property hs_call_body="Confirmed $50K budget, Q2 timeline." \
  --property hs_call_direction=OUTBOUND \
  --property hs_call_status=COMPLETED \
  --property hs_call_duration=1800000 \
  --property hs_timestamp=$(date +%s)000 \
  --format json | jq -r '.id')
hubspot associations create --from calls:$call_id --to contacts:149
hubspot associations create --from calls:$call_id --to deals:456

# NOTE
note_id=$(hubspot objects create --type notes \
  --property hs_note_body="Sent proposal. Follow-up Friday." \
  --property hs_timestamp=$(date +%s)000 \
  --format json | jq -r '.id')
hubspot associations create --from notes:$note_id --to deals:456

# MEETING — start/end in Unix ms; reuse start as hs_timestamp
start=$(date +%s)000; end=$(( ${start%000} + 3600 ))000
meeting_id=$(hubspot objects create --type meetings \
  --property hs_meeting_title="Demo — Acme" --property hs_meeting_outcome=COMPLETED \
  --property hs_meeting_start_time=$start --property hs_meeting_end_time=$end \
  --property hs_timestamp=$start --format json | jq -r '.id')
hubspot associations create --from meetings:$meeting_id --to contacts:149

# TASK — hs_timestamp is the DUE DATE, not creation time
due=$(( $(date -v+7d +%s) * 1000 ))   # macOS; Linux: date -d '7 days' +%s
task_id=$(hubspot objects create --type tasks \
  --property hs_task_subject="Confirm proposal received" \
  --property hs_task_priority=HIGH \
  --property hs_task_status=NOT_STARTED \
  --property hs_task_type=CALL \
  --property hs_timestamp=$due \
  --format json | jq -r '.id')
hubspot associations create --from tasks:$task_id --to deals:456

Open tasks for a contact — two CLI calls, no xargs

associations list emits {"id","type"} per row; objects get reads from stdin in one batch call (see bulk-operations/SKILL.md "Read in batch").

hubspot associations list --from contacts:149 --to tasks \
| hubspot objects get --type tasks \
    --properties hs_task_subject,hs_task_status,hs_task_priority,hs_timestamp \
| jq -c 'select(.properties.hs_task_status != "COMPLETED")'

Bulk: follow-up task per deal in a stage

The deal ID and the task ID must travel together. Persist the deal payload to a file, create tasks (output order matches input order — see bulk-operations), then zip the two ID lists line-by-line and stream association pairs in one call.

due=$(( $(date -v+7d +%s) * 1000 ))

# 1. Per-deal payload, deal_id retained alongside the create payload.
hubspot objects search --type deals --filter "dealstage=appointmentscheduled" \
  --properties dealname \
| jq -c --argjson due "$due" '{deal_id: .id, payload: {properties: {
    hs_task_subject: ("Follow up: " + .properties.dealname),
    hs_task_priority: "HIGH", hs_task_status: "NOT_STARTED", hs_task_type: "CALL",
    hs_timestamp: ($due|tostring)
  }}}' > /tmp/deal_tasks.jsonl

# 2. Create tasks; one CLI call for the whole batch.
jq -c '.payload' /tmp/deal_tasks.jsonl \
| hubspot objects create --type tasks > /tmp/created_tasks.jsonl

# 3. Zip and stream association pairs through stdin.
paste \
  <(jq -r '.deal_id' /tmp/deal_tasks.jsonl) \
  <(jq -r '.id'      /tmp/created_tasks.jsonl) \
| jq -Rc 'split("\t") | {from:("tasks:"+.[1]), to:("deals:"+.[0])}' \
| hubspot associations create

For >100 rows, apply the dry-run / digest / confirm pattern from bulk-operations/SKILL.md.

Known constraints

Activities must be associated immediately or they're invisible in the CRM UI. properties get doesn't return enum option values for activity types — use the reference. No sequences/cadences in the CLI.


Supporting file: resources/activity-properties-reference.md

Activity Properties — Quick Reference

Property names and enum values for hubspot objects create --type {calls|notes|meetings|tasks}. Kept here because hubspot properties list --type calls is noisy (~80 props) and hubspot properties get does not expose enum option values today — so the values below are not otherwise discoverable from the CLI. Verify against the portal if a value is rejected.

calls

PropertyTypeNotes
hs_call_titlestring
hs_call_bodystring (HTML ok)
hs_call_directionenumINBOUND OUTBOUND
hs_call_statusenumBUSY CALLING_CRM_USER CANCELED COMPLETED CONNECTING FAILED IN_PROGRESS MISSED NO_ANSWER QUEUED RINGING
hs_call_durationnumberMilliseconds (60000 = 1 min)
hs_call_dispositionstringPortal-defined outcome code
hs_timestampnumberRequired. Unix ms — when the call happened

notes

PropertyTypeNotes
hs_note_bodystring (HTML ok)Required.
hs_timestampnumberRequired. Unix ms

meetings

PropertyTypeNotes
hs_meeting_titlestring
hs_meeting_bodystring (HTML ok)
hs_meeting_outcomeenumSCHEDULED COMPLETED RESCHEDULED NO_SHOW CANCELLED
hs_meeting_start_timenumberUnix ms
hs_meeting_end_timenumberUnix ms
hs_meeting_locationstringAddress or video URL
hs_timestampnumberRequired. Unix ms — primary timeline timestamp

tasks

PropertyTypeNotes
hs_task_subjectstringRequired. Title in the task queue
hs_task_bodystring
hs_task_statusenumNOT_STARTED IN_PROGRESS COMPLETED DEFERRED WAITING
hs_task_priorityenumLOW MEDIUM HIGH
hs_task_typeenumTODO CALL EMAIL
hs_timestampnumberRequired. Unix ms — due date for tasks

Association targets

ActivityValid --to types
callscontacts, deals, companies, tickets
notescontacts, deals, companies, tickets
meetingscontacts, deals, companies
taskscontacts, deals, companies

Supporting file: bulk-operations/SKILL.md

Resources

FileWhen to use
resources/json-patterns.mdReshape 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):

hubspot feedback "batch upsert timed out on 5k rows" --source agent

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:

{"id":"123","properties":{"email":"jane@example.com","firstname":"Jane"},"createdAt":"...","updatedAt":"...","archived":false,"url":"..."}

--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.

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.

# Positional args (small, known list)
hubspot objects get --type contacts 12345 67890 23456 --properties email,firstname

# Stdin from another command — one CLI call total
hubspot associations list --from companies:67890 --to contacts \
| jq -c '{id}' \
| hubspot objects get --type contacts --properties email,firstname,jobtitle

# Bare IDs on stdin also work
printf '12345\n67890\n23456\n' | hubspot objects get --type contacts --properties email

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.

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:

bash resources/pagination-loop.sh <object_type> <output_file> [properties] [extra_flags...]

Examples:

# All contacts with specific properties
bash resources/pagination-loop.sh contacts /tmp/contacts.jsonl email,firstname,lastname

# Search with a filter (passes extra flags through to the CLI)
bash resources/pagination-loop.sh contacts /tmp/leads.jsonl email,firstname '--filter' 'lifecyclestage=lead'

# All deals, default properties
bash resources/pagination-loop.sh deals /tmp/deals.jsonl

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.

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 commandRequired 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

Every destructive op (delete, merge, bulk update) supports --dry-run. The gating depends on row count:

≤100 rows — dry-run emits one preview line per record:

{"ok":true,"dry_run":true,"executed":false,"mutation_kind":"RecordMutation","command":"objects delete contacts","target":{"kind":"contacts_record","id":"123","name":"123"}}

Re-run without --dry-run to execute.

>100 rows — dry-run emits a single BulkData line with a digest and an apply_command_hint:

{"ok":true,"dry_run":true,"executed":false,"mutation_kind":"BulkData","portal":"123456","target":{"name":"202 records"},"impact":{"records_affected":202,"reversible":false},"digest":"blast-29cfdd48b583","expires_in_seconds":300,"apply_command_hint":"hubspot objects delete contacts --digest blast-29cfdd48b583 --confirm '202'"}

You must re-run with --digest <hash> --confirm <value> within 5 minutes. The confirm value is the record count (deletes) or the secondary ID (merge). Read it off apply_command_hint.

Three-step pattern:

# 1. Preview
hubspot objects search --type contacts --filter "lifecyclestage=subscriber" \
| jq -c '{id}' \
| hubspot objects delete --type contacts --dry-run \
| tee /tmp/preview.jsonl

# 2. Lift the digest + confirm value (only present for >100 rows)
digest=$(jq -r 'select(.mutation_kind=="BulkData") | .digest' /tmp/preview.jsonl)
confirm=$(jq -r 'select(.mutation_kind=="BulkData") | .impact.records_affected' /tmp/preview.jsonl)

# 3. Execute — re-pipe the SAME inputs
hubspot objects search --type contacts --filter "lifecyclestage=subscriber" \
| jq -c '{id}' \
| hubspot objects delete --type contacts --digest "$digest" --confirm "$confirm"

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:

hubspot history --since 1h --format table
hubspot history --since 24h --kind BulkData       # only bulk ops
hubspot history --since 7d --kind MetadataDestroy # schema deletes

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.

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:

cat external.jsonl \
| jq -c '{idProperty:"email", id:.email, properties:{firstname:.first, lastname:.last, company:.company}}' \
| hubspot objects upsert --type contacts --dry-run

# Or set idProperty once:
cat external.jsonl \
| jq -c '{id:.email, properties:{firstname:.first}}' \
| hubspot objects upsert --type contacts --id-property email

Rate-limit hygiene

There is no true batch endpoint behind update/delete/upsert — the CLI issues one API call per stdin line. Test with head -n 50 before piping a 50k-row file. If the API starts 429ing, the per-line output will show {"ok":false,"error":{"status":429,...}} — split your input file and retry the failed lines.

Common reshapes

See resources/json-patterns.md for the full set. The two you need 90% of the time:

# Read → update payload
hubspot objects search --type contacts --filter "industry=Tech" \
| jq -c '{id, properties:{lifecyclestage:"marketingqualifiedlead"}}' \
| hubspot objects update --type contacts

# Search → delete list
hubspot objects search --type contacts --filter "!email" \
| jq -c '{id}' \
| hubspot objects delete --type contacts --dry-run

Known constraints

  • Some destructive operations may be blocked under user-OAuth (browser login); set HUBSPOT_ACCESS_TOKEN (private app token) when running deletes if the CLI returns a permission error.
  • hubspot owners list returns CRM users; there is no teams object. For team-level operations, group by hubspot_owner_id client-side.
  • No Lists API, no sequences/cadences API in the current CLI surface.

How do I install CALL in Cursor, Claude Code, or Codex?

Run npx skills add hubspot/agent-cli-skills --skill sales-execution in the project where you want it, then ask your agent for the skill by name. The --skill flag installs only CALL, not every skill in the repository.

Where does CALL come from and what license is it under?

CALL comes from the hubspot/agent-cli-skills repository on GitHub. That repository has 21 GitHub stars. The skill is published under the Apache-2.0 license.

Prefer plain text? Read the CALL guide as markdown.