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.
In this repository18 other files it gives its agents.
- audience-targetingSkill
- bulk-operationsSkill
- communication-historySkill
- conversation-history-briefSkill
- conversation-inbox-triageSkill
- crm-data-qualitySkill
- crm-lookupSkill
- customer-retentionSkill
- custom-object-managementSkill
- data-enrichmentSkill
- quote-to-cashSkill
- sales-executionSkill
- sales-reportingSkill
- team-ownershipSkill
- ticket-resolutionSkill
- visitor-identificationSkill
- workflow-automationSkill
- workSkill
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
- Very specific: line 29
Read bulk-operations/SKILL.md first — JSONL piping, batch read, pagination, and the dry-run/digest/confirm flow live there.
≈ “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.
Files
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,"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_datePipe 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-run5. 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 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.
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
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.

