customer-retention — Claude Code skill: churn search and follow-ups
Finds customers with no recent contact or at-risk subscriptions through HubSpot CRM filters, then creates follow-up tasks in bulk.
npx skills add HubSpot/agent-cli-skills --skill customer-retention -a claude-codeWhat customer-retention does
The skill sits on top of bulk-operations: that is where the JSONL pipe, pagination and --dry-run behaviour come from. It starts by pulling inactive contacts — for instance lifecyclestage=customer with notes_last_contacted older than 60 days, or contacts never touched at all. Subscriptions marked past_due get their own pass, along with the association back to the contact.
Tasks come next. A search writes the cohort to a file, jq builds one task payload per contact, hubspot objects create writes them in a single call, and hubspot associations create links each task to its contact — up to 100 pairs per API call. An unassociated task stays invisible in the CRM UI, so the linking step is not optional.
Portal schemas differ, so verify properties with hubspot properties get before filtering. A 403 on subscriptions means the token lacks subscriptions-read; use a private-app token that carries the scope. There is no built-in churn or health score property, so you track that with a custom one.
What it helps with
- find customers with no outreach in 60 days
- pull contacts that were never contacted
- flag subscriptions with a past-due payment
- create call tasks for a whole cohort in one pass
- associate new tasks with contacts so they show up in the CRM
Example prompts for the agent
- find customers not contacted in 60 days and create call tasks
- show past_due subscriptions and their contacts
- build the inactive cohort and create HIGH priority tasks
How to install customer-retention
1Skills CLI (Vercel)
npx skills add HubSpot/agent-cli-skills --skill customer-retention -a claude-code2Manually: put the files in place
~/.claude/skills/customer-retention/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/customer-retention/
1Skills CLI (Vercel)
npx skills add HubSpot/agent-cli-skills --skill customer-retention -a codex2Manually: put the files in place
.agents/skills/customer-retention/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add HubSpot/agent-cli-skills --skill customer-retention -a cursor2Manually: put the files in place
.cursor/skills/customer-retention/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install HubSpot/agent-cli-skills customer-retention2Manually: put the files in place
.github/skills/customer-retention/SKILL.md1Skills CLI (Vercel)
npx skills add HubSpot/agent-cli-skills --skill customer-retention -a gemini-cli2Platform CLI
gemini skills install https://github.com/HubSpot/agent-cli-skills --path customer-retentionManually: put the files in place
.devin/skills/customer-retention/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add HubSpot/agent-cli-skills --skill customer-retention -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about customer-retention
How do I install customer-retention in Claude?
Run npx skills add HubSpot/agent-cli-skills --skill customer-retention -a claude-code. The skill lands at ~/.claude/skills/customer-retention/SKILL.md.
What does customer-retention do?
It finds inactive and at-risk customers through HubSpot CRM filters and creates follow-up tasks, either one at a time or for a whole cohort.
What do I need before using it?
Read bulk-operations/SKILL.md first — every read and write relies on its pipe and pagination. Checking subscriptions needs a token with the subscriptions-read scope.
Source text
SKILL.md · Apache-2.0
---
name: customer-retention
description: Identify inactive/at-risk customers via CRM filters and create follow-up tasks at scale. Builds on `bulk-operations`; defers activity-creation specifics to `sales-execution`.
triggers:
- "customer retention"
- "churn risk"
- "inactive customers"
- "customer follow-up"
- "at-risk accounts"
- "customers not contacted"
- "renewal"
- "account health"
---
## Resources
| File | When to use |
|---|---|
| `resources/customer-health-signals.md` | Filter cookbook of churn signals — `--filter` expressions for `notes_last_contacted`, `hs_last_sales_activity_date`, `hs_email_optout`, stale tickets, subscription status. |
## Prereqs
Read `bulk-operations/SKILL.md` first — every read/write below uses its JSONL pipe, pagination, and dry-run/digest patterns. Activity-property tables and association rules live in `sales-execution/SKILL.md`.
Schema is portal-specific. Verify each property before filtering — e.g. `hubspot properties get --type contacts notes_last_contacted`, `... hs_last_sales_activity_date`, `... --type subscriptions hs_subscription_status`. If `subscriptions` returns 403, your token lacks `subscriptions-read` — use a private-app token with that scope.
## 1 — Find inactive customers
```bash
CUTOFF=$(date -v-60d +%Y-%m-%d 2>/dev/null || date -d '60 days ago' +%Y-%m-%d)
# No outreach in 60d (calls/notes/meetings update notes_last_contacted)
hubspot objects search --type contacts \
--filter "lifecyclestage=customer AND notes_last_contacted<$CUTOFF" \
--properties email,firstname,notes_last_contacted,hubspot_owner_id
# No sales activity in 60d (broader — also catches emails/tasks)
hubspot objects search --type contacts \
--filter "lifecyclestage=customer AND hs_last_sales_activity_date<$CUTOFF" \
--properties email,firstname,hs_last_sales_activity_date
# Never contacted
hubspot objects search --type contacts \
--filter "lifecyclestage=customer AND !notes_last_contacted" \
--properties email,firstname
```
For more signals (email opt-out, stale tickets, no open deals) see `resources/customer-health-signals.md`. For >100 hits, use the pagination loop from `bulk-operations`.
To audit *why* a customer churned or moved stage — which workflow, integration, import, or UI edit last changed `lifecyclestage` — use `hubspot objects history --type contacts --properties lifecyclestage [--id <recordId>] [--include-ui]`. It flattens each property's source history into one row per change.
## 2 — Flag at-risk subscriptions
`subscriptions` is a standard object (`hubspot objects types` confirms). Enum values for `hs_subscription_status` are portal-specific — verify before filtering, then plug the exact value in:
```bash
hubspot properties options-list --type subscriptions hs_subscription_status # allowed values
# Past-due — revenue at immediate risk (substitute your verified value)
hubspot objects search --type subscriptions \
--filter "hs_subscription_status=past_due" \
--properties hs_recurring_billing_total,hs_subscription_status
# Map an at-risk subscription to its contact for outreach
hubspot associations list --from subscriptions:<sub_id> --to contacts --format jsonl
```
## 3 — Create a follow-up task or check-in note
Activity creation lives in `sales-execution` (full property tables, note + meeting flows). One anchor example — unassociated tasks are invisible in the CRM UI, so always associate:
```bash
task_id=$(hubspot objects create --type tasks \
--property hs_task_subject="Q1 retention check-in" \
--property hs_task_priority=HIGH --property hs_task_status=NOT_STARTED \
--property hs_task_type=CALL --property hs_timestamp=$(date +%s)000 \
--format json | jq -r '.id')
hubspot associations create --from tasks:$task_id --to contacts:<contact_id>
```
## 4 — Bulk task creation for a cohort
Pipe a search through `jq` into one `objects create` call, then associate. `objects create` is Additive — never digest-gated — so `--dry-run` is a plain preview and you execute by dropping it (no digest/confirm). The digest flow only applies to the irreversible writes in `bulk-operations/SKILL.md`.
```bash
DUE_MS=$(( ($(date +%s) + 2*86400) * 1000 )) # due in 2 days
# 1. Capture the cohort (same file feeds both create + associate)
hubspot objects search --type contacts \
--filter "lifecyclestage=customer AND notes_last_contacted<$CUTOFF" \
--properties firstname > /tmp/inactive.jsonl
# 2. Build task payloads — one per contact
jq -c --arg due "$DUE_MS" '{
contact_id: .id,
properties: {
hs_task_subject: ("Re-engage: " + (.properties.firstname // "customer")),
hs_task_priority: "HIGH", hs_task_status: "NOT_STARTED",
hs_task_type: "CALL", hs_timestamp: $due
}
}' /tmp/inactive.jsonl > /tmp/task_payloads.jsonl
# 3. Dry-run, then create (drop contact_id before piping)
jq -c '{properties}' /tmp/task_payloads.jsonl | hubspot objects create --type tasks --dry-run | head
jq -c '{properties}' /tmp/task_payloads.jsonl | hubspot objects create --type tasks > /tmp/created.jsonl
# 4. Associate each new task to its contact — one piped call (associations create batches 100 pairs/API call; paste preserves order)
paste <(jq -r '.id' /tmp/created.jsonl) <(jq -r '.contact_id' /tmp/task_payloads.jsonl) \
| jq -cR 'split("\t") | {from:("tasks:" + .[0]), to:("contacts:" + .[1])}' \
| hubspot associations create
```
One CLI call for the search, one for the create, and one for all associations — no `xargs -I{}` per record. The output-order guarantee of `objects create` (one result per stdin line, in order — see `bulk-operations` "Output shape") is what makes the `paste` correct.
## Known gaps
- No native churn-score / health-score property — track via a custom property.
- `hubspot segments` provides CRM lists for re-engagement cohorts — `segments members-list` pulls a list's members, and `segments create` / `update-filters` save an at-risk audience as a reusable list. Re-engagement enrollment can be built as a workflow via `hubspot workflows create` / `update` (see `workflow-automation/SKILL.md`).
- `hubspot sequences` reads Sales Hub sequences (read-only): use `sequences enrollments <contact_id>` to see whether an at-risk customer was ever engaged through a sales sequence. Sequences are a product API (Sales Hub Professional+, `automation.sequences.read` scope), not a CRM object type — the CLI cannot create sequences or enroll contacts. This list grows over time; recheck `hubspot --help` / `CHANGELOG.md` rather than assuming an API is absent.
- `hubspot associations create` batches stdin JSONL (up to 100 pairs per API call) — pipe the pairs in one call instead of looping.
© HubSpot · Apache-2.0 · HubSpot/agent-cli-skills