Deal Management

hubspot/agent-cli-skills/deal-management

by hubspota8eea0880838No license27 starsListed Oct 8, 2026Updated Oct 8, 2026Repository updated 7 days ago

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.

Instructions onlyMarketing & Sales
AI-generated overview

Runs the HubSpot deal lifecycle from the CLI: discover pipelines, qualify MQLs, bulk-advance or reassign deals, find stalled deals, and close.

What it does
This skill provides command-line recipes for managing HubSpot deals end to end: discovering portal-specific pipelines and stages, qualifying marketing-qualified leads into deals with contact and company associations, and promoting lifecycle stages. It also covers bulk stage advances and owner reassignments, queries for stalled or past-close-date deals, and closing deals with stage and close-date updates. All bulk changes follow a dry-run, digest, and confirm flow, with two resource files for stage progression and stalled-deal filters.
When to use it
Use it when working with HubSpot deals from the CLI, such as converting MQLs into deals, moving or reassigning deals in bulk, hunting stalled or overdue deals, or closing deals. It assumes the HubSpot CLI and its bulk-operations conventions are already in place.
Requirements
Requires the HubSpot CLI (hubspot commands), jq, and shell tools such as paste and date; access to a HubSpot portal with deal, contact, company, and owner data. It ships no scripts, only instructions and two markdown resource files, and references companion skills (bulk-operations, sales-execution, sales-reporting).

Resources

FileWhen to use
resources/lifecycle-stage-progression.mdLifecycle stage API values + the contact-side updates that pair with deal moves.
resources/stalled-deal-queries.mdFilter 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 MQLshubspot 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, promotehubspot 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 IDhubspot objects update --type contacts <contact_id> \  --property lifecyclestage=salesqualifiedlead --property hs_lead_status=OPEN_DEAL --dry-runhubspot 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 orderjq -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 contactsjq -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 callpaste <(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/confirmjq -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. previewhubspot 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/--confirmhubspot 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 stepsOLD=$(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.jsonldigest=$(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 duehubspot 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 IDhubspot objects update --type deals <deal_id> \  --property dealstage=closedwon --property closedate=2026-05-15 --dry-runhubspot 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.

Source and attribution

Source:hubspot/agent-cli-skillsindeal-managementat commita8eea08

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal