Skip to main content
skill CX Codex

deal-management — Claude Code skill for the HubSpot deal lifecycle

Runs HubSpot deals from the terminal: finds MQLs, creates deals with associations, moves stages in bulk and closes stalled ones.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add HubSpot/agent-cli-skills --skill deal-management -a claude-code
Open on GitHub

What deal-management does

The skill drives the hubspot CLI through the whole deal lifecycle. It starts by listing pipelines and stages with hubspot pipelines list --type deals --format jsonl and hubspot pipelines stages. Stage IDs are portal-specific, so it discovers them at runtime instead of hardcoding them.

Qualifying an MQL takes several steps: search contacts by lifecycle stage and lead status, create the deal, associate it with the contact and company, then promote the contact's lifecycle stage. For volume there is a jq and JSONL pipeline: snapshot the MQLs to a file, create one deal per line, pair contacts and deals with paste, then associate in one call. If any create fails, the skill tells you to abort, because null deal IDs would otherwise be zipped onto real contacts.

Object updates are irreversible, so the skill makes you run --dry-run first, capture the --digest, and only then confirm with --confirm. Read bulk-operations/SKILL.md before starting: JSONL piping, batch reads, pagination and the dry-run/digest/confirm flow all live there. Company associations need a separate pass per contact, since a contact may have zero companies or several.

What it helps with

  • find MQLs with no deal and turn them into deals in bulk
  • associate a new deal with its contact and company
  • move every deal in one stage to the next
  • filter stalled deals and deals past their close date
  • promote a contact's lifecycle stage after the deal is created

Example prompts for the agent

  • Find every MQL with no deal and create one deal per contact
  • Move all deals in Qualified To Buy to the next stage
  • Show deals past their close date with no recent activity

How to install deal-management

1Skills CLI (Vercel)

npx skills add HubSpot/agent-cli-skills --skill deal-management -a claude-code

2Manually: put the files in place

~/.claude/skills/deal-management/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/deal-management/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about deal-management

How do I install deal-management in Claude?

Run npx skills add HubSpot/agent-cli-skills --skill deal-management -a claude-code. The skill lands in ~/.claude/skills/deal-management/SKILL.md.

What does deal-management do?

It covers the full deal lifecycle from the command line: discovering pipelines and stages, qualifying MQLs into deals with associations, advancing and reassigning in bulk, hunting stalled deals, and closing.

Why can't I hardcode stage IDs?

Pipeline and stage IDs belong to a specific portal. The skill discovers them on every run because another portal may rename or remove those stages.

Source text

SKILL.md · Apache-2.0
---
name: deal-management
description: Run the full deal lifecycle from CLI — discover pipelines/stages, qualify MQLs into deals with associations, advance/reassign in bulk, hunt stalled deals, and close.
triggers:
  - "create deal"
  - "qualify lead"
  - "MQL to SQL"
  - "qualify MQL"
  - "deal from contact"
  - "advance deal"
  - "move deal stage"
  - "reassign deal"
  - "stalled deals"
  - "deals past close date"
  - "accelerate pipeline"
  - "close deal"
  - "deal lifecycle"
---

## Resources

| File | When to use |
|---|---|
| `resources/lifecycle-stage-progression.md` | Lifecycle stage API values + the contact-side updates that pair with deal moves. |
| `resources/stalled-deal-queries.md` | Filter cookbook for stalled / no-activity / past-close-date deals with dynamic dates. |

## Foundations

Read `bulk-operations/SKILL.md` first — JSONL piping, batch read, pagination, and the dry-run/digest/confirm flow live there. Reshape recipes are in `bulk-operations/resources/json-patterns.md`. `hubspot <command> --help` is the source of truth. Object types are plural (`contacts`, `deals`, `companies`). For property reference: `hubspot properties list --type deals` — don't hardcode property tables.

## 1. Discover pipelines and stages

Pipeline and stage IDs are **portal-specific**. Always discover at runtime — never hardcode across portals.

```bash
hubspot pipelines list --type deals --format jsonl
# {"id":"default","label":"Sales Pipeline","displayOrder":0,"stages":[...]}
# {"id":"a1b2c3d4-0000-0000-0000-000000000000","label":"Enterprise Pipeline","displayOrder":1,"stages":[...]}

hubspot pipelines stages --type deals --pipeline default --format jsonl
# {"id":"appointmentscheduled","label":"Appointment Scheduled","displayOrder":0,"metadata":{"isClosed":"false","probability":"0.2"}}
# {"id":"qualifiedtobuy","label":"Qualified To Buy","displayOrder":1,"metadata":{"isClosed":"false","probability":"0.4"}}
# ...
# {"id":"closedwon","label":"Closed Won","displayOrder":5,"metadata":{"isClosed":"true","probability":"1.0"}}
# {"id":"closedlost","label":"Closed Lost","displayOrder":6,"metadata":{"isClosed":"true","probability":"0.0"}}
```

`pipelines list`/`get` embed each pipeline's `stages`, so a single call gives you the
stage id -> label map you need to translate a deal's `dealstage` GUID:

```bash
hubspot pipelines get --type deals default --format jsonl \
  | jq -r '.stages[] | "\(.id)\t\(.label)"'
```

Grab a specific stage ID by label:

```bash
QUALIFIED=$(hubspot pipelines stages --type deals --pipeline default --format jsonl \
  | jq -r 'select(.label=="Qualified To Buy") | .id')
```

The IDs shown above (`appointmentscheduled`, `closedwon`, etc.) are HubSpot's standard `default` deal pipeline stages — but discover yours every run since portals can rename or remove them.

## 2. Qualify an MQL into a deal

Find connected MQLs without a deal, then for each: create the deal, associate to contact + company, promote lifecycle.

```bash
# 1. find ready MQLs
hubspot objects search --type contacts \
  --filter "lifecyclestage=marketingqualifiedlead AND hs_lead_status=CONNECTED AND num_associated_deals=0" \
  --properties email,firstname,lastname,company,hubspot_owner_id

# 2. for one contact: company lookup, deal create, associate, promote
hubspot associations list --from contacts:<contact_id> --to companies   # → <company_id>

hubspot objects create --type deals \
  --property "dealname=Acme Corp - Inbound" \
  --property pipeline=default --property dealstage=qualifiedtobuy \
  --property amount=0 --property hubspot_owner_id=<owner_id>
# returns {"id":"<deal_id>","ok":true,...}

hubspot associations create --from deals:<deal_id> --to contacts:<contact_id>
hubspot associations create --from deals:<deal_id> --to companies:<company_id>

# lifecycle promote — objects update is irreversible, so dry-run then confirm with the contact ID
hubspot objects update --type contacts <contact_id> \
  --property lifecyclestage=salesqualifiedlead --property hs_lead_status=OPEN_DEAL --dry-run
hubspot objects update --type contacts <contact_id> \
  --property lifecyclestage=salesqualifiedlead --property hs_lead_status=OPEN_DEAL \
  --digest <hash> --confirm <contact_id>
```

### Bulk pattern — many MQLs at once

`objects create` returns one result line per stdin line, in input order. Capture both streams and join by line for associations:

```bash
# 1. snapshot MQLs to a file (preserves order for the join)
hubspot objects search --type contacts \
  --filter "lifecyclestage=marketingqualifiedlead AND hs_lead_status=CONNECTED AND num_associated_deals=0" \
  --properties email,firstname,lastname,company,hubspot_owner_id \
  > /tmp/mqls.jsonl

# 2. one deal per MQL — output preserves order
jq -c '{properties:{
    dealname: ((.properties.firstname // "") + " " + (.properties.lastname // "") + " - " + (.properties.company // "Unknown")),
    pipeline:"default", dealstage:"qualifiedtobuy", amount:"0", dealtype:"newbusiness",
    hubspot_owner_id:(.properties.hubspot_owner_id // "")
  }}' /tmp/mqls.jsonl \
| hubspot objects create --type deals > /tmp/deals.jsonl

# 3. abort if any create failed — paste would zip null deal IDs onto real contacts
jq -e 'select(.ok==false)' /tmp/deals.jsonl > /dev/null && { echo "Some deal creates failed — inspect /tmp/deals.jsonl" >&2; exit 1; }

# 4. pair contact <-> new deal by line for the association call
paste <(jq -r '.id' /tmp/mqls.jsonl) <(jq -r '.id' /tmp/deals.jsonl) \
| jq -cR 'split("\t") | {from:("deals:" + .[1]), to:("contacts:" + .[0])}' \
| hubspot associations create

# 5. promote lifecycle on every contact — objects update is irreversible, so dry-run then re-pipe with the digest/confirm
jq -c '{id, properties:{lifecyclestage:"salesqualifiedlead", hs_lead_status:"OPEN_DEAL"}}' /tmp/mqls.jsonl \
| hubspot objects update --type contacts --dry-run \
| tee /tmp/promote.preview.jsonl

digest=$(jq -r 'select(.digest != null) | .digest' /tmp/promote.preview.jsonl)
confirm=$(jq -r 'select(.digest != null) | .target.id' /tmp/promote.preview.jsonl)   # batch: row count

jq -c '{id, properties:{lifecyclestage:"salesqualifiedlead", hs_lead_status:"OPEN_DEAL"}}' /tmp/mqls.jsonl \
| hubspot objects update --type contacts --digest "$digest" --confirm "$confirm"
```

Company associations need a separate per-contact pass via `hubspot associations list --from contacts:<id> --to companies` — a contact may have zero or many companies.

Pre-qualification checks are just filters on the search: has email, has a company, no open deal, has an owner — all in the `--filter` already. See `resources/lifecycle-stage-progression.md` for the full stage progression and contact-side updates.

## 3. Advance or reassign in bulk

```bash
# move every deal in one stage to the next — 1. preview
hubspot objects search --type deals --filter "dealstage=qualifiedtobuy" \
| jq -c '{id, properties:{dealstage:"presentationscheduled"}}' \
| hubspot objects update --type deals --dry-run \
| tee /tmp/advance.preview.jsonl

# 2. lift the digest + confirm (present at every size)
digest=$(jq -r 'select(.digest != null) | .digest' /tmp/advance.preview.jsonl)
confirm=$(jq -r 'select(.digest != null) | .target.id' /tmp/advance.preview.jsonl)   # batch: row count; single: record ID

# 3. execute — re-pipe the SAME inputs plus --digest/--confirm
hubspot objects search --type deals --filter "dealstage=qualifiedtobuy" \
| jq -c '{id, properties:{dealstage:"presentationscheduled"}}' \
| hubspot objects update --type deals --digest "$digest" --confirm "$confirm"

# reassign open deals from one rep to another — same three steps
OLD=$(hubspot owners list --format jsonl | jq -r 'select(.email=="[email protected]") | .id')
NEW=$(hubspot owners list --format jsonl | jq -r 'select(.email=="[email protected]") | .id')
hubspot objects search --type deals --filter "hubspot_owner_id=$OLD AND hs_is_closed!=true" \
| jq -c "{id, properties:{hubspot_owner_id:\"$NEW\"}}" \
| hubspot objects update --type deals --dry-run \
| tee /tmp/deal-reassign.preview.jsonl
digest=$(jq -r 'select(.digest != null) | .digest' /tmp/deal-reassign.preview.jsonl)
confirm=$(jq -r 'select(.digest != null) | .target.id' /tmp/deal-reassign.preview.jsonl)
hubspot objects search --type deals --filter "hubspot_owner_id=$OLD AND hs_is_closed!=true" \
| jq -c "{id, properties:{hubspot_owner_id:\"$NEW\"}}" \
| hubspot objects update --type deals --digest "$digest" --confirm "$confirm"
```

The dry-run emits a digest at every size (confirm = the row count for a batch, the record ID for a single); re-pipe with `--digest <hash> --confirm <value>` lifted from the preview line. Full flow in `bulk-operations/SKILL.md`.

## 4. Find stalled deals

Filter cookbook with dynamic dates lives in `resources/stalled-deal-queries.md`. The core query:

```bash
# open deals with no activity in 30 days (macOS / Linux date examples in resources)
hubspot objects search --type deals \
  --filter "hs_last_activity_date<$(date -v-30d +%Y-%m-%d) AND hs_is_closed!=true" \
  --properties dealname,dealstage,closedate,hubspot_owner_id,hs_last_activity_date
```

Pipe the result into an update (extend close dates, move stage, set a flag) or into task creation. For follow-up tasks/calls/notes against stalled deals, see the `sales-execution` skill — don't duplicate activity-object property handling here.

```bash
# extend close dates for everything past due
hubspot objects search --type deals \
  --filter "closedate<$(date +%Y-%m-%d) AND hs_is_closed!=true" \
| jq -c '{id, properties:{closedate:"2026-06-30"}}' \
| hubspot objects update --type deals --dry-run
```

## 5. Close

Closing is a stage update + `closedate` (YYYY-MM-DD). `hs_is_closed` and `hs_is_closed_won` are read-only — HubSpot derives them from the stage.

```bash
# single — objects update is irreversible, so dry-run then confirm with the deal ID
hubspot objects update --type deals <deal_id> \
  --property dealstage=closedwon --property closedate=2026-05-15 --dry-run
hubspot objects update --type deals <deal_id> \
  --property dealstage=closedwon --property closedate=2026-05-15 --digest <hash> --confirm <deal_id>

# bulk — preview first, then re-pipe with the digest/confirm from the preview (confirm = row count)
hubspot objects search --type deals --filter "dealstage=contractsent AND hubspot_owner_id=<owner_id>" \
| jq -c '{id, properties:{dealstage:"closedwon", closedate:"2026-05-15"}}' \
| hubspot objects update --type deals --dry-run \
| tee /tmp/close.preview.jsonl

digest=$(jq -r 'select(.digest != null) | .digest' /tmp/close.preview.jsonl)
confirm=$(jq -r 'select(.digest != null) | .target.id' /tmp/close.preview.jsonl)

hubspot objects search --type deals --filter "dealstage=contractsent AND hubspot_owner_id=<owner_id>" \
| jq -c '{id, properties:{dealstage:"closedwon", closedate:"2026-05-15"}}' \
| hubspot objects update --type deals --digest "$digest" --confirm "$confirm"
```

Win/loss analysis (close reasons, win rate, ARR roll-up) is in the `sales-reporting` skill.

## Known constraints

- Bulk MQL → deal needs a two-pass shell flow: associations must be built from `objects create` output, not in the same pipe.
- `lifecyclestage` is forward-only in most portal settings — backward transitions may be rejected.
- `closedate` is a date string (`YYYY-MM-DD`). Datetime activity props (`hs_last_activity_date`) also accept a date string for `<`/`>` comparisons.
- `hubspot sequences` reads Sales Hub sequences (`list` / `get` / `enrollments`) but is read-only — the CLI cannot enroll a contact in a sequence, so create a follow-up task via `sales-execution` instead. `sequences enrollments <contact_id>` is useful for win/loss context on a deal's contacts.

© HubSpot · Apache-2.0 · HubSpot/agent-cli-skills

Similar skills

All skills for Automation and no-code: n8n, Zapier, Make →

From the same set11

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor