---
name: "CALL"
repo: https://github.com/hubspot/agent-cli-skills
source: https://github.com/hubspot/agent-cli-skills/blob/HEAD/sales-execution/SKILL.md
license: "Apache-2.0"
install: "npx skills add hubspot/agent-cli-skills --skill sales-execution"
page: https://100xlab.com/resources/agent-skills/hubspot-agent-cli-skills--sales-execution--y5zaw9
---

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

```bash
# 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").

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

```bash
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

| 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`):

```bash
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:

```json
{"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.

```bash
# 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
bash resources/pagination-loop.sh <object_type> <output_file> [properties] [extra_flags...]
```

Examples:

```bash
# 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 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:
```json
{"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`:
```json
{"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:

```bash
# 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:

```bash
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:

```bash
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:

```bash
# 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.
