PluginBench
Skill
Pass
Audit score 90

sales-reporting

hubspot/agent-cli-skills

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

What is sales-reporting?

Generates sales reports directly from HubSpot via the command line, including deals closing this week, open pipeline by stage and owner, and closed-won vs closed-lost analysis. Use this to pull quick snapshots of pipeline health, track deal velocity, and measure win rates without leaving the terminal.

  • Query deals closing in the next 7 days with owner and amount
  • Summarize open pipeline by stage and by owner with deal counts and total value
  • Analyze closed-won vs closed-lost deals over a custom date range
  • Calculate win rates and win value by sales rep
  • Generate revenue reports by close month for won deals
  • Filter and aggregate deals using HubSpot's search API with jq reshaping

How to install sales-reporting

npx skills add https://github.com/hubspot/agent-cli-skills --skill sales-reporting
Prerequisites
  • HubSpot account with API access (user OAuth or service key)
  • hubspot CLI installed and authenticated (hubspot auth login or service key configured)
  • jq installed for JSON reshaping and aggregation
  • GNU or macOS date command for date range calculations
Claude Code
Cursor
Windsurf
Cline

How to use sales-reporting

  1. 1.Run npx skills add https://github.com/hubspot/agent-cli-skills --skill sales-reporting to install
  2. 2.Authenticate with HubSpot: hubspot auth login (user OAuth) or configure a service key
  3. 3.Use hubspot objects search --type deals with --filter and --properties flags to query deals
  4. 4.Pipe results to jq to group, aggregate, and format output (see examples for daily briefing, pipeline by stage/owner, and win/loss analysis)
  5. 5.For owner names, run hubspot owners list once and join the output to replace numeric owner IDs

Use cases

Good for
  • Morning standup: pull deals closing this week and deals updated in the last 24 hours
  • Pipeline health check: view open deals grouped by stage or owner to spot bottlenecks
  • Quarterly win/loss review: compare closed-won and closed-lost deals for a period and calculate win rates by rep
  • Monthly revenue forecast: aggregate closed-won deals by close month to track bookings
  • Sales rep performance: rank reps by win rate, deal count, and won value
Who it's for
  • Sales managers and directors reviewing pipeline and team performance
  • Individual sales reps tracking their own deal velocity and win rate
  • Revenue operations teams building automated reporting workflows
  • Sales leadership preparing for forecasting and board reviews

sales-reporting FAQ

Why do some numeric properties return empty strings instead of null?

HubSpot CRM properties can be unset as either null or an empty string. Always guard tonumber with select(. != null and . != "") to avoid errors.

How do I map stage IDs to stage names?

Run hubspot pipelines stages --type deals --pipeline <id> to get the portal-specific stage mapping. Note: this command requires an app token and will 403 under user OAuth.

Can I get reports for a specific date range?

Yes. Use closedate filters in the --filter flag, e.g., closedate>=2026-04-01 AND closedate<2026-07-01. Adjust the date format to YYYY-MM-DD.

How do I resolve numeric owner IDs to names?

Run hubspot owners list to dump owner data, then join by ID in jq or save to a TSV file for reference.

What happens if I hit the 100-row limit on search results?

Results of exactly 100 rows are almost always truncated. Use the bulk-operations skill's pagination rules to fetch all records before aggregating.

Full instructions (SKILL.md)

Source of truth, from hubspot/agent-cli-skills.


name: sales-reporting description: Daily briefings, pipeline snapshots, and win/loss analysis from the terminal — closing-this-week, open pipeline by stage/owner, and closed-won vs closed-lost over a period. triggers:

  • "daily briefing"
  • "pipeline snapshot"
  • "deals closing this week"
  • "deals by owner"
  • "win rate"
  • "closed won"
  • "closed lost"
  • "win/loss analysis"
  • "revenue by month"
  • "pipeline by stage"

Source of truth

hubspot <command> --help is authoritative. Build on bulk-operations/SKILL.md — JSONL shape, batch-read rules, and pagination live there. Reshape patterns: bulk-operations/resources/json-patterns.md. search/list cap at 100 rows per call; a result of exactly 100 is almost always truncated — paginate via bulk-operations/SKILL.md before aggregating.

Property and output shape notes

  • All CRM property values come back as strings in JSONL — booleans included. hs_is_closed_won is returned as "true"/"false" (string); amount is a numeric string. Use tonumber for arithmetic; compare booleans as strings (== "true") when filtering client-side.
  • Numeric properties can be null or an empty string ("") when unset/blank. tonumber aborts on "". Always guard with select(. != null and . != "") | tonumber.
  • In --filter expressions, hs_is_closed_won=true and hs_is_closed!=true work — the API parses the value.
  • --properties returns the standard nested shape: {"id":"123","properties":{"amount":"5000","dealname":"..."}}. Reference fields as .properties.amount in jq.
  • Stage IDs in dealstage are portal-specific. Map them with hubspot pipelines stages --type deals --pipeline <id>. hubspot pipelines is app-token-only — see Auth section; it 403s under user OAuth.
  • hubspot_owner_id is a numeric string. Resolve to a name with hubspot owners list (fields: id, firstName, lastName, email). hubspot owners list works under both user OAuth (hubspot auth login) and a service key.

1. Daily briefing

Date windows differ between macOS and GNU date:

# macOS
TODAY=$(date +%Y-%m-%d); NEXT_7=$(date -v+7d +%Y-%m-%d); YESTERDAY=$(date -v-1d +%Y-%m-%d)
# Linux
TODAY=$(date +%Y-%m-%d); NEXT_7=$(date -d '7 days' +%Y-%m-%d); YESTERDAY=$(date -d '1 day ago' +%Y-%m-%d)

Deals closing in the next 7 days:

hubspot objects search --type deals \
  --filter "closedate>$TODAY AND closedate<$NEXT_7 AND hs_is_closed!=true" \
  --properties dealname,amount,closedate,hubspot_owner_id

Deals updated in the last 24h:

hubspot objects search --type deals \
  --filter "hs_lastmodifieddate>$YESTERDAY AND hs_is_closed!=true" \
  --properties dealname,amount,dealstage,hs_lastmodifieddate

Open-pipeline summary line:

hubspot objects search --type deals --filter "hs_is_closed!=true" --properties amount \
| jq -rs '{count: length, value: ([.[].properties.amount | select(. != null and . != "") | tonumber] | add // 0 | round)}
          | "Open pipeline: \(.count) deals, $\(.value)"'

2. Pipeline snapshot

By stage — count and amount per dealstage:

hubspot objects search --type deals --filter "hs_is_closed!=true" \
  --properties dealstage,amount \
| jq -rs '
    group_by(.properties.dealstage)
    | map({stage: .[0].properties.dealstage, count: length,
           total: ([.[].properties.amount | select(. != null and . != "") | tonumber] | add // 0 | round)})
    | sort_by(-.total) | .[] | "\(.stage)\tcount: \(.count)\tvalue: $\(.total)"' \
| column -t -s$'\t'

By owner:

hubspot objects search --type deals --filter "hs_is_closed!=true" \
  --properties amount,hubspot_owner_id \
| jq -rs '
    group_by(.properties.hubspot_owner_id)
    | map({owner: .[0].properties.hubspot_owner_id, count: length,
           total: ([.[].properties.amount | select(. != null and . != "") | tonumber] | add // 0 | round)})
    | sort_by(-.total) | .[] | "owner \(.owner)\tdeals: \(.count)\tvalue: $\(.total)"' \
| column -t -s$'\t'

To label owner IDs with names, dump the owners file once and join:

hubspot owners list | jq -r '"\(.id)\t\(.firstName) \(.lastName) <\(.email)>"' > /tmp/owners.tsv

3. Win/loss analysis

Filter on hs_is_closed_won=true for won; hs_is_closed=true AND hs_is_closed_won!=true for lost. Scope with closedate>=YYYY-MM-DD AND closedate<YYYY-MM-DD.

Closed won / lost in a period:

hubspot objects search --type deals \
  --filter "hs_is_closed_won=true AND closedate>=2026-04-01 AND closedate<2026-07-01" \
  --properties dealname,amount,closedate,hubspot_owner_id

hubspot objects search --type deals \
  --filter "hs_is_closed=true AND hs_is_closed_won!=true AND closedate>=2026-04-01 AND closedate<2026-07-01" \
  --properties dealname,amount,closedate,hubspot_owner_id

Win rate by rep — pull all closed deals in the period, group, divide. Note: hs_is_closed_won lands as a string, so compare == "true".

hubspot objects search --type deals \
  --filter "hs_is_closed=true AND closedate>=2026-01-01" \
  --properties hubspot_owner_id,hs_is_closed_won,amount \
| jq -rs '
    group_by(.properties.hubspot_owner_id)
    | map({owner: .[0].properties.hubspot_owner_id,
           total: length,
           won: ([.[] | select(.properties.hs_is_closed_won == "true")] | length),
           won_value: ([.[] | select(.properties.hs_is_closed_won == "true")
                       | .properties.amount | select(. != null and . != "") | tonumber] | add // 0 | round)})
    | map(. + {win_rate: ((.won / .total * 100) | round)})
    | sort_by(-.won_value)
    | .[] | "owner \(.owner)\twon: \(.won)/\(.total)\trate: \(.win_rate)%\twon: $\(.won_value)"' \
| column -t -s$'\t'

Revenue by close month (won deals):

hubspot objects search --type deals \
  --filter "hs_is_closed_won=true AND closedate>=2026-01-01" \
  --properties amount,closedate \
| jq -rs '
    group_by(.properties.closedate[0:7])
    | map({month: .[0].properties.closedate[0:7], count: length,
           revenue: ([.[].properties.amount | select(. != null and . != "") | tonumber] | add // 0 | round)})
    | sort_by(.month) | .[] | "\(.month)\tdeals: \(.count)\trevenue: $\(.revenue)"' \
| column -t -s$'\t'

Known limitations

  • hubspot pipelines stages does not expose stage probability — won/lost stages can't be auto-identified from the stages list. Use hs_is_closed_won on deals instead.
  • No team object — group by hubspot_owner_id and resolve names from hubspot owners list client-side.
  • hubspot pipelines is app-token-only and 403s under user OAuth. hubspot owners list works under user OAuth. Keep raw IDs + warn; do not fail the report when pipelines is unavailable.
  • Numeric CRM properties can be null or ""; always guard tonumber with select(. != null and . != "").