deal-management
hubspot/agent-cli-skills
Run the full deal lifecycle from CLI — discover pipelines, qualify MQLs, advance/reassign in bulk, hunt stalled deals, and close.
What is deal-management?
Manage HubSpot deals end-to-end from the command line. Discover portal-specific pipelines and stages, convert MQLs to deals with automatic associations, bulk-move deals between stages, reassign ownership, identify stalled opportunities, and close won/lost deals. Use this when you need to automate deal workflows, qualify leads at scale, or hunt pipeline health issues.
- Discover pipelines and stages dynamically at runtime (never hardcoded)
- Qualify MQLs into deals with automatic contact and company associations
- Bulk advance deals between stages or reassign ownership across reps
- Find stalled deals with no activity or past close dates using dynamic date filters
- Close deals (won/lost) with stage updates and close-date tracking
- Extend close dates and update deal properties in bulk with dry-run preview
How to install deal-management
npx skills add https://github.com/hubspot/agent-cli-skills --skill deal-management- HubSpot account with deals enabled
- hubspot CLI installed and authenticated
- Familiarity with bulk-operations skill (JSONL piping, dry-run/digest/confirm flow)
How to use deal-management
- 1.Run `hubspot pipelines list --type deals` to discover your portal's pipeline IDs and labels
- 2.Run `hubspot pipelines stages --type deals --pipeline <id>` to list available stages for your pipeline
- 3.Search for MQLs with `hubspot objects search --type contacts --filter "lifecyclestage=marketingqualifiedlead AND num_associated_deals=0"` to find conversion candidates
- 4.Create deals and associate them to contacts/companies using the bulk MQL pattern (create deals, pair by line, associate, promote lifecycle)
- 5.Use `hubspot objects search --type deals --filter "dealstage=<stage>"` piped to `jq` and `objects update` to bulk-move deals between stages
- 6.Find stalled deals with `hubspot objects search --type deals --filter "hs_last_activity_date<$(date -v-30d +%Y-%m-%d) AND hs_is_closed!=true"`
- 7.Always run bulk updates with `--dry-run` first; use `--digest` and `--confirm` for large batches (>100 rows)
- 8.Close deals by updating `dealstage` to `closedwon` or `closedlost` and setting `closedate` to YYYY-MM-DD format
Use cases
- Convert a batch of marketing-qualified leads into sales deals with proper associations and lifecycle promotion
- Move all deals in 'Qualified To Buy' stage to 'Presentation Scheduled' and preview changes before confirming
- Reassign all open deals from a departing rep to a new owner in one operation
- Identify deals stalled for 30+ days with no activity and bulk-extend their close dates
- Close multiple won deals at month-end with proper stage and date updates
- Sales operations managers automating deal workflows
- Sales leaders managing rep transitions and deal reassignments
- Revenue operations teams qualifying leads at scale
- Sales reps hunting stalled opportunities and pipeline health
deal-management FAQ
Pipeline and stage IDs are portal-specific and can be renamed or removed. Hardcoding them breaks when you switch portals or admins change the pipeline structure. Always query at runtime.
Search for MQLs without deals, create one deal per MQL (preserving order), pair contact and deal IDs by line number, then bulk-associate and promote lifecycle. See the bulk pattern in section 2 — associations must be built from create output, not in the same pipe.
--dry-run shows what would change without applying it. For >100 rows, --dry-run emits a digest hash; re-run with --digest <hash> --confirm <count> to apply. This prevents accidental bulk updates.
Use `hubspot objects search --type deals --filter "hs_last_activity_date<$(date -v-30d +%Y-%m-%d) AND hs_is_closed!=true"` to find open deals inactive for 30+ days. See resources/stalled-deal-queries.md for more dynamic date examples.
No — these are read-only. HubSpot derives them from the dealstage. To close a deal, update dealstage to closedwon or closedlost and set closedate.
Full instructions (SKILL.md)
Source of truth, from hubspot/agent-cli-skills.
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.
hubspot pipelines list --type deals --format jsonl
# {"id":"default","label":"Sales Pipeline","displayOrder":0}
# {"id":"a1b2c3d4-0000-0000-0000-000000000000","label":"Enterprise Pipeline","displayOrder":1}
hubspot pipelines stages --type deals --pipeline default --format jsonl
# {"id":"appointmentscheduled","label":"Appointment Scheduled","displayOrder":0}
# {"id":"qualifiedtobuy","label":"Qualified To Buy","displayOrder":1}
# ...
# {"id":"closedwon","label":"Closed Won","displayOrder":5}
# {"id":"closedlost","label":"Closed Lost","displayOrder":6}
Grab a specific stage ID by label:
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.
# 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>
hubspot objects update --type contacts <contact_id> \
--property lifecyclestage=salesqualifiedlead --property hs_lead_status=OPEN_DEAL
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:
# 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
jq -c '{id, properties:{lifecyclestage:"salesqualifiedlead", hs_lead_status:"OPEN_DEAL"}}' /tmp/mqls.jsonl \
| hubspot objects update --type contacts
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
# move every deal in one stage to the next — preview, then re-run without --dry-run
hubspot objects search --type deals --filter "dealstage=qualifiedtobuy" \
| jq -c '{id, properties:{dealstage:"presentationscheduled"}}' \
| hubspot objects update --type deals --dry-run
# reassign open deals from one rep to another
OLD=$(hubspot owners list --format jsonl | jq -r 'select(.email=="old@co.com") | .id')
NEW=$(hubspot owners list --format jsonl | jq -r 'select(.email=="new@co.com") | .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
For >100 rows, the dry-run emits a digest line; re-pipe with --digest <hash> --confirm <count>. 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:
# 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.
# 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.
# single
hubspot objects update --type deals <deal_id> \
--property dealstage=closedwon --property closedate=2026-05-15
# bulk — preview first
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
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 createoutput, not in the same pipe. lifecyclestageis forward-only in most portal settings — backward transitions may be rejected.closedateis a date string (YYYY-MM-DD). Datetime activity props (hs_last_activity_date) also accept a date string for</>comparisons.hubspot sequencesreads 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 viasales-executioninstead.sequences enrollments <contact_id>is useful for win/loss context on a deal's contacts.
Related skills
More from hubspot/agent-cli-skills and the wider catalog.

quote-to-cash
Build product catalogs, assemble quotes with line items, and track invoices and subscriptions through revenue.

sales-execution
Log calls, notes, meetings, and tasks against contacts and deals in HubSpot CRM.

sales-reporting
Daily briefings, pipeline snapshots, and win/loss analysis from the terminal.

team-ownership
Assign, reassign, and audit CRM record ownership across contacts, companies, deals, and tickets.

ticket-resolution
Create, triage, advance, and close HubSpot support tickets with pipeline discovery and bulk operations.

workflow-automation
Manage HubSpot v4 workflows (list, create, update, delete) via the hubspot agent CLI.