AGENT SKILLBY HUBSPOT
CALL
AT A GLANCE
CALL is a go-to-market agent skill from hubspot. 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 hubspot/agent-cli-skills repository on GitHub has 21 stars and the Apache-2.0 license. To add it, run npx skills add hubspot/agent-cli-skills --skill sales-execution
- SOURCE REPOSITORY
- hubspot/agent-cli-skills (opens GitHub in a new tab)
- REPO STARS
- SOURCE REPOSITORY
- LICENSE
- Apache-2.0
- CATEGORIES
- SKILL.MD SIZE
- 16 KB367 LINES
QUICK ANSWER
- WHAT IT IS01
- 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.
- INPUTS02
- Context for go-to-market work: your goals, audience, constraints, and any source material the skill asks for.
- OUTPUT03
- A ready-to-use result for go-to-market work: the analysis, copy, or recommendations the agent produces.
INSTALL
One command adds this skill from its source repository.
npx skills add hubspot/agent-cli-skills --skill sales-executionREAD THE SKILL.MD BELOW BEFORE YOU RUN IT. SKILLS INSTRUCT YOUR AGENT.
SKILL.MD
The full file, as published in the source repository. Copy it, download it or open it on GitHub.
ON THIS PAGE · 27
Resources#
| File | When to use |
|---|---|
resources/activity-properties-reference.md | Property 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.
| Path | Field | Format |
|---|---|---|
objects create --property hs_timestamp=... | hs_timestamp | Unix ms (13 digits) |
objects get --type calls <id> returns | properties.hs_timestamp | Unix ms (string) |
activities list --contact <id> returns | timestamp (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:456Open 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 createFor >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#
| Property | Type | Notes |
|---|---|---|
hs_call_title | string | |
hs_call_body | string (HTML ok) | |
hs_call_direction | enum | INBOUND OUTBOUND |
hs_call_status | enum | BUSY CALLING_CRM_USER CANCELED COMPLETED CONNECTING FAILED IN_PROGRESS MISSED NO_ANSWER QUEUED RINGING |
hs_call_duration | number | Milliseconds (60000 = 1 min) |
hs_call_disposition | string | Portal-defined outcome code |
hs_timestamp | number | Required. Unix ms — when the call happened |
notes#
| Property | Type | Notes |
|---|---|---|
hs_note_body | string (HTML ok) | Required. |
hs_timestamp | number | Required. Unix ms |
meetings#
| Property | Type | Notes |
|---|---|---|
hs_meeting_title | string | |
hs_meeting_body | string (HTML ok) | |
hs_meeting_outcome | enum | SCHEDULED COMPLETED RESCHEDULED NO_SHOW CANCELLED |
hs_meeting_start_time | number | Unix ms |
hs_meeting_end_time | number | Unix ms |
hs_meeting_location | string | Address or video URL |
hs_timestamp | number | Required. Unix ms — primary timeline timestamp |
tasks#
| Property | Type | Notes |
|---|---|---|
hs_task_subject | string | Required. Title in the task queue |
hs_task_body | string | |
hs_task_status | enum | NOT_STARTED IN_PROGRESS COMPLETED DEFERRED WAITING |
hs_task_priority | enum | LOW MEDIUM HIGH |
hs_task_type | enum | TODO CALL EMAIL |
hs_timestamp | number | Required. Unix ms — due date for tasks |
Association targets#
| Activity | Valid --to types |
|---|---|
| calls | contacts, deals, companies, tickets |
| notes | contacts, deals, companies, tickets |
| meetings | contacts, deals, companies |
| tasks | contacts, deals, companies |
Supporting file: bulk-operations/SKILL.md#
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):
hubspot feedback "batch upsert timed out on 5k rows" --source agentThis 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 emailA 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:
- 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:
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.jsonlThe 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 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#
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 deleteshistory 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 emailRate-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-runKnown 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 listreturns CRM users; there is noteamsobject. For team-level operations, group byhubspot_owner_idclient-side.- No Lists API, no sequences/cadences API in the current CLI surface.
FAQ
07 QUESTIONS
01What is the CALL agent skill?
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. It is listed under go-to-market in the 100xLab agent skills directory.
02How 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.
03What does the CALL SKILL.md cover?
The SKILL.md includes sections on Resources, The two non-obvious rules, Create + associate, by type, Open tasks for a contact — two CLI calls, no xargs, Bulk: follow-up task per deal in a stage and Known constraints. Read the full text on this page or in plain markdown at /resources/agent-skills/hubspot-agent-cli-skills--sales-execution--y5zaw9/raw.
04Which agents can use CALL?
CALL installs with the cross-agent skills installer (npx skills add), which sets it up for agents such as Claude Code, Cursor and Codex. Each agent loads the SKILL.md when you ask for the skill by name.
05What license is CALL under?
The hubspot/agent-cli-skills repository declares the Apache-2.0 license. Read the license file in the repository before you copy or redistribute the skill.
06Is CALL free to use?
Reading, copying and installing CALL costs nothing: it is published in a public GitHub repository under the Apache-2.0 license. Reuse and redistribution follow that license.
07Where is the source of CALL?
CALL comes from https://github.com/hubspot/agent-cli-skills, which has 21 GitHub stars. The SKILL.md is at https://github.com/hubspot/agent-cli-skills/blob/HEAD/sales-execution/SKILL.md. The author is hubspot.
Need an agent workflow built around your stack?
Tell us the workflow you want automated. We reply within 4 working hours (IST).
ATTRIBUTION
CALL was written by its original authors and is published in hubspot/agent-cli-skills (opens GitHub in a new tab) under the Apache-2.0 license. All rights stay with the original authors.
100xLab did not write, maintain or endorse this skill. This page lists it for discovery. Repo stars belong to the source repository, not to the skill. Review any skill before you install it.