agentleFS
HubSpotHubSpot/agent-cli-skills/deal-management/SKILL.md

deal-management

Skill28 starsChanged 43 days agoApache-2.0View on GitHub

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.

The file

We split this skill into the rules it gives an agent

1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Read bulk-operations/SKILL.md first — JSONL piping, batch read, pagination, and the dry-run/digest/confirm flow live there.

    line 29

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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.

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:

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

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>

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

# 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

# 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=="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 \
| 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:

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

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.