Source of truth is authoritative. Build on — JSONL shape, batch-read rules, and pagination live there. Reshape patterns: . / cap at 100 rows per call; a result of exactly 100 is almost always truncated — paginate via before aggregating. Property and output shape notes - All CRM property values come back as strings in JSONL — booleans included. is returned as / (string); is a numeric string. Use for arithmetic; compare booleans as strings ( ) when filtering client-side. - Numeric properties can be or an empty string ( ) when unset/blank. aborts on . Always guard with . - In expressions, and wo…

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

Source of truth is authoritative. Build on — JSONL shape, batch-read rules, and pagination live there. Reshape patterns: . / cap at 100 rows per call; a result of exactly 100 is almost always truncated — paginate via before aggregating. Property and output shape notes - All CRM property values come back as strings in JSONL — booleans included. is returned as / (string); is a numeric string. Use for arithmetic; compare booleans as strings ( ) when filtering client-side. - Numeric properties can be or an empty string ( ) when unset/blank. aborts on . Always guard with . - In expressions, and wo…

\\t'\n```\n\nTo label owner IDs with names, dump the owners file once and join:\n```bash\nhubspot owners list | jq -r '\"\\(.id)\\t\\(.firstName) \\(.lastName) \u003c\\(.email)>\"' > /tmp/owners.tsv\n```\n\n## 3. Win/loss analysis\n\nFilter 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\u003cYYYY-MM-DD`.\n\n**Closed won / lost in a period:**\n```bash\nhubspot objects search --type deals \\\n --filter \"hs_is_closed_won=true AND closedate>=2026-04-01 AND closedate\u003c2026-07-01\" \\\n --properties dealname,amount,closedate,hubspot_owner_id\n\nhubspot objects search --type deals \\\n --filter \"hs_is_closed=true AND hs_is_closed_won!=true AND closedate>=2026-04-01 AND closedate\u003c2026-07-01\" \\\n --properties dealname,amount,closedate,hubspot_owner_id\n```\n\n**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\"`.\n```bash\nhubspot objects search --type deals \\\n --filter \"hs_is_closed=true AND closedate>=2026-01-01\" \\\n --properties hubspot_owner_id,hs_is_closed_won,amount \\\n| jq -rs '\n group_by(.properties.hubspot_owner_id)\n | map({owner: .[0].properties.hubspot_owner_id,\n total: length,\n won: ([.[] | select(.properties.hs_is_closed_won == \"true\")] | length),\n won_value: ([.[] | select(.properties.hs_is_closed_won == \"true\")\n | .properties.amount | select(. != null and . != \"\") | tonumber] | add // 0 | round)})\n | map(. + {win_rate: ((.won / .total * 100) | round)})\n | sort_by(-.won_value)\n | .[] | \"owner \\(.owner)\\twon: \\(.won)/\\(.total)\\trate: \\(.win_rate)%\\twon: $\\(.won_value)\"' \\\n| column -t -s

Source of truth is authoritative. Build on — JSONL shape, batch-read rules, and pagination live there. Reshape patterns: . / cap at 100 rows per call; a result of exactly 100 is almost always truncated — paginate via before aggregating. Property and output shape notes - All CRM property values come back as strings in JSONL — booleans included. is returned as / (string); is a numeric string. Use for arithmetic; compare booleans as strings ( ) when filtering client-side. - Numeric properties can be or an empty string ( ) when unset/blank. aborts on . Always guard with . - In expressions, and wo…

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

Source of truth is authoritative. Build on — JSONL shape, batch-read rules, and pagination live there. Reshape patterns: . / cap at 100 rows per call; a result of exactly 100 is almost always truncated — paginate via before aggregating. Property and output shape notes - All CRM property values come back as strings in JSONL — booleans included. is returned as / (string); is a numeric string. Use for arithmetic; compare booleans as strings ( ) when filtering client-side. - Numeric properties can be or an empty string ( ) when unset/blank. aborts on . Always guard with . - In expressions, and wo…

\\t'\n```\n\n## 4. Saved reports\n\nThe `hubspot reports` family runs HubSpot's saved reports and CRM-SQL reports server-side, so you don't have to recompute aggregates client-side:\n\n- `reports list` / `get \u003cid>` — browse and inspect saved reports.\n- `reports fetch-dataset \u003cid>` — re-execute a saved report server-side and return its dataset.\n- `reports create \"\u003cCRM SQL>\"` — create a report from a CRM-SQL query (a companion skill can generate the SQL); `reports insights \u003cid>` generates AI insights (async, polls to completion).\n- `reports update \u003cid>` / `clone \u003cid>` / `favorite \u003cid>` / `unfavorite \u003cid>` — manage report metadata.\n- `reports delete \u003cid>` — irreversible, digest-gated: `--dry-run` first, then `--digest \u003chash> --confirm \"\u003creport name>\"` (confirm = the report's name).\n\nRun `hubspot reports --help` for the full surface and the CRM-SQL grammar.\n\n## 5. Win/loss context: sequence enrollments\n\n`hubspot sequences enrollments \u003ccontact_id>` returns a contact's Sales Hub sequence enrollment history (read-only) — useful for attributing wins/losses to outreach. Each row: `{\"contactId\":789,\"enrollments\":[{\"sequenceName\":\"Q4 Outbound\",\"state\":\"FINISHED\",\"enrolledAt\":\"...\",\"currentStepOrder\":5,\"totalSteps\":6}]}`. Join it to a deal's associated contacts for a \"was this deal worked through a sequence?\" view. See `hubspot sequences enrollments --help`.\n\n## Known limitations\n\n- Won/lost stages are identifiable from `hubspot pipelines stages`: each stage's `metadata` carries `isClosed`/`probability` (e.g. `jq -r 'select(.metadata.probability==\"1.0\") | .id'`). `hs_is_closed_won` on the deal itself also works.\n- No team object — group by `hubspot_owner_id` and resolve names from `hubspot owners list` client-side.\n- `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.\n- Numeric CRM properties can be `null` or `\"\"`; always guard `tonumber` with `select(. != null and . != \"\")`.","attachment_filenames":[],"attachments":[],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":2},"content":[{"text":"Source of truth","type":"text"}]},{"type":"paragraph","content":[{"text":"hubspot \u003ccommand> --help","type":"text","marks":[{"type":"code_inline"}]},{"text":" is authoritative. Build on ","type":"text"},{"text":"bulk-operations/SKILL.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — JSONL shape, batch-read rules, and pagination live there. Reshape patterns: ","type":"text"},{"text":"bulk-operations/resources/json-patterns.md","type":"text","marks":[{"type":"code_inline"}]},{"text":". ","type":"text"},{"text":"search","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"list","type":"text","marks":[{"type":"code_inline"}]},{"text":" cap at 100 rows per call; a result of exactly 100 is almost always truncated — paginate via ","type":"text"},{"text":"bulk-operations/SKILL.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" before aggregating.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Property and output shape notes","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"All CRM property values come back as ","type":"text"},{"text":"strings","type":"text","marks":[{"type":"strong"}]},{"text":" in JSONL — booleans included. ","type":"text"},{"text":"hs_is_closed_won","type":"text","marks":[{"type":"code_inline"}]},{"text":" is returned as ","type":"text"},{"text":"\"true\"","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"\"false\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" (string); ","type":"text"},{"text":"amount","type":"text","marks":[{"type":"code_inline"}]},{"text":" is a numeric string. Use ","type":"text"},{"text":"tonumber","type":"text","marks":[{"type":"code_inline"}]},{"text":" for arithmetic; compare booleans as strings (","type":"text"},{"text":"== \"true\"","type":"text","marks":[{"type":"code_inline"}]},{"text":") when filtering client-side.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Numeric properties can be ","type":"text","marks":[{"type":"strong"}]},{"text":"null","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" ","type":"text","marks":[{"type":"strong"}]},{"text":"or","type":"text","marks":[{"type":"strong"},{"type":"em"}]},{"text":" an empty string (","type":"text","marks":[{"type":"strong"}]},{"text":"\"\"","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":")","type":"text","marks":[{"type":"strong"}]},{"text":" when unset/blank. ","type":"text"},{"text":"tonumber","type":"text","marks":[{"type":"code_inline"}]},{"text":" aborts on ","type":"text"},{"text":"\"\"","type":"text","marks":[{"type":"code_inline"}]},{"text":". Always guard with ","type":"text"},{"text":"select(. != null and . != \"\") | tonumber","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"In ","type":"text"},{"text":"--filter","type":"text","marks":[{"type":"code_inline"}]},{"text":" expressions, ","type":"text"},{"text":"hs_is_closed_won=true","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"hs_is_closed!=true","type":"text","marks":[{"type":"code_inline"}]},{"text":" work — the API parses the value.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--properties","type":"text","marks":[{"type":"code_inline"}]},{"text":" returns the standard nested shape: ","type":"text"},{"text":"{\"id\":\"123\",\"properties\":{\"amount\":\"5000\",\"dealname\":\"...\"}}","type":"text","marks":[{"type":"code_inline"}]},{"text":". Reference fields as ","type":"text"},{"text":".properties.amount","type":"text","marks":[{"type":"code_inline"}]},{"text":" in jq.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Stage IDs in ","type":"text"},{"text":"dealstage","type":"text","marks":[{"type":"code_inline"}]},{"text":" are portal-specific. Map them with ","type":"text"},{"text":"hubspot pipelines stages --type deals --pipeline \u003cid>","type":"text","marks":[{"type":"code_inline"}]},{"text":" (or read the ","type":"text"},{"text":"stages","type":"text","marks":[{"type":"code_inline"}]},{"text":" array embedded in ","type":"text"},{"text":"hubspot pipelines list/get","type":"text","marks":[{"type":"code_inline"}]},{"text":"). ","type":"text"},{"text":"hubspot pipelines","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" is app-token-only","type":"text","marks":[{"type":"strong"}]},{"text":" — see Auth section; it 403s under user OAuth.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"hubspot_owner_id","type":"text","marks":[{"type":"code_inline"}]},{"text":" is a numeric string. Resolve to a name with ","type":"text"},{"text":"hubspot owners list","type":"text","marks":[{"type":"code_inline"}]},{"text":" (fields: ","type":"text"},{"text":"id","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"firstName","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"lastName","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"email","type":"text","marks":[{"type":"code_inline"}]},{"text":"). ","type":"text"},{"text":"hubspot owners list","type":"text","marks":[{"type":"code_inline"}]},{"text":" works under both user OAuth (","type":"text"},{"text":"hubspot auth login","type":"text","marks":[{"type":"code_inline"}]},{"text":") and a service key.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"1. Daily briefing","type":"text"}]},{"type":"paragraph","content":[{"text":"Date windows differ between macOS and GNU ","type":"text"},{"text":"date","type":"text","marks":[{"type":"code_inline"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# macOS\nTODAY=$(date +%Y-%m-%d); NEXT_7=$(date -v+7d +%Y-%m-%d); YESTERDAY=$(date -v-1d +%Y-%m-%d)\n# Linux\nTODAY=$(date +%Y-%m-%d); NEXT_7=$(date -d '7 days' +%Y-%m-%d); YESTERDAY=$(date -d '1 day ago' +%Y-%m-%d)","type":"text"}]},{"type":"paragraph","content":[{"text":"Deals closing in the next 7 days:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"hubspot objects search --type deals \\\n --filter \"closedate>$TODAY AND closedate\u003c$NEXT_7 AND hs_is_closed!=true\" \\\n --properties dealname,amount,closedate,hubspot_owner_id","type":"text"}]},{"type":"paragraph","content":[{"text":"Deals updated in the last 24h:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"hubspot objects search --type deals \\\n --filter \"hs_lastmodifieddate>$YESTERDAY AND hs_is_closed!=true\" \\\n --properties dealname,amount,dealstage,hs_lastmodifieddate","type":"text"}]},{"type":"paragraph","content":[{"text":"Open-pipeline summary line:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"hubspot objects search --type deals --filter \"hs_is_closed!=true\" --properties amount \\\n| jq -rs '{count: length, value: ([.[].properties.amount | select(. != null and . != \"\") | tonumber] | add // 0 | round)}\n | \"Open pipeline: \\(.count) deals, $\\(.value)\"'","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"2. Pipeline snapshot","type":"text"}]},{"type":"paragraph","content":[{"text":"By stage","type":"text","marks":[{"type":"strong"}]},{"text":" — count and amount per ","type":"text"},{"text":"dealstage","type":"text","marks":[{"type":"code_inline"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"hubspot objects search --type deals --filter \"hs_is_closed!=true\" \\\n --properties dealstage,amount \\\n| jq -rs '\n group_by(.properties.dealstage)\n | map({stage: .[0].properties.dealstage, count: length,\n total: ([.[].properties.amount | select(. != null and . != \"\") | tonumber] | add // 0 | round)})\n | sort_by(-.total) | .[] | \"\\(.stage)\\tcount: \\(.count)\\tvalue: $\\(.total)\"' \\\n| column -t -s

Source of truth is authoritative. Build on — JSONL shape, batch-read rules, and pagination live there. Reshape patterns: . / cap at 100 rows per call; a result of exactly 100 is almost always truncated — paginate via before aggregating. Property and output shape notes - All CRM property values come back as strings in JSONL — booleans included. is returned as / (string); is a numeric string. Use for arithmetic; compare booleans as strings ( ) when filtering client-side. - Numeric properties can be or an empty string ( ) when unset/blank. aborts on . Always guard with . - In expressions, and wo…

\\t'","type":"text"}]},{"type":"paragraph","content":[{"text":"By owner:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"hubspot objects search --type deals --filter \"hs_is_closed!=true\" \\\n --properties amount,hubspot_owner_id \\\n| jq -rs '\n group_by(.properties.hubspot_owner_id)\n | map({owner: .[0].properties.hubspot_owner_id, count: length,\n total: ([.[].properties.amount | select(. != null and . != \"\") | tonumber] | add // 0 | round)})\n | sort_by(-.total) | .[] | \"owner \\(.owner)\\tdeals: \\(.count)\\tvalue: $\\(.total)\"' \\\n| column -t -s

Source of truth is authoritative. Build on — JSONL shape, batch-read rules, and pagination live there. Reshape patterns: . / cap at 100 rows per call; a result of exactly 100 is almost always truncated — paginate via before aggregating. Property and output shape notes - All CRM property values come back as strings in JSONL — booleans included. is returned as / (string); is a numeric string. Use for arithmetic; compare booleans as strings ( ) when filtering client-side. - Numeric properties can be or an empty string ( ) when unset/blank. aborts on . Always guard with . - In expressions, and wo…

\\t'","type":"text"}]},{"type":"paragraph","content":[{"text":"To label owner IDs with names, dump the owners file once and join:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"hubspot owners list | jq -r '\"\\(.id)\\t\\(.firstName) \\(.lastName) \u003c\\(.email)>\"' > /tmp/owners.tsv","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"3. Win/loss analysis","type":"text"}]},{"type":"paragraph","content":[{"text":"Filter on ","type":"text"},{"text":"hs_is_closed_won=true","type":"text","marks":[{"type":"code_inline"}]},{"text":" for won; ","type":"text"},{"text":"hs_is_closed=true AND hs_is_closed_won!=true","type":"text","marks":[{"type":"code_inline"}]},{"text":" for lost. Scope with ","type":"text"},{"text":"closedate>=YYYY-MM-DD AND closedate\u003cYYYY-MM-DD","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Closed won / lost in a period:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"hubspot objects search --type deals \\\n --filter \"hs_is_closed_won=true AND closedate>=2026-04-01 AND closedate\u003c2026-07-01\" \\\n --properties dealname,amount,closedate,hubspot_owner_id\n\nhubspot objects search --type deals \\\n --filter \"hs_is_closed=true AND hs_is_closed_won!=true AND closedate>=2026-04-01 AND closedate\u003c2026-07-01\" \\\n --properties dealname,amount,closedate,hubspot_owner_id","type":"text"}]},{"type":"paragraph","content":[{"text":"Win rate by rep","type":"text","marks":[{"type":"strong"}]},{"text":" — pull all closed deals in the period, group, divide. Note: ","type":"text"},{"text":"hs_is_closed_won","type":"text","marks":[{"type":"code_inline"}]},{"text":" lands as a string, so compare ","type":"text"},{"text":"== \"true\"","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"hubspot objects search --type deals \\\n --filter \"hs_is_closed=true AND closedate>=2026-01-01\" \\\n --properties hubspot_owner_id,hs_is_closed_won,amount \\\n| jq -rs '\n group_by(.properties.hubspot_owner_id)\n | map({owner: .[0].properties.hubspot_owner_id,\n total: length,\n won: ([.[] | select(.properties.hs_is_closed_won == \"true\")] | length),\n won_value: ([.[] | select(.properties.hs_is_closed_won == \"true\")\n | .properties.amount | select(. != null and . != \"\") | tonumber] | add // 0 | round)})\n | map(. + {win_rate: ((.won / .total * 100) | round)})\n | sort_by(-.won_value)\n | .[] | \"owner \\(.owner)\\twon: \\(.won)/\\(.total)\\trate: \\(.win_rate)%\\twon: $\\(.won_value)\"' \\\n| column -t -s

Source of truth is authoritative. Build on — JSONL shape, batch-read rules, and pagination live there. Reshape patterns: . / cap at 100 rows per call; a result of exactly 100 is almost always truncated — paginate via before aggregating. Property and output shape notes - All CRM property values come back as strings in JSONL — booleans included. is returned as / (string); is a numeric string. Use for arithmetic; compare booleans as strings ( ) when filtering client-side. - Numeric properties can be or an empty string ( ) when unset/blank. aborts on . Always guard with . - In expressions, and wo…

\\t'","type":"text"}]},{"type":"paragraph","content":[{"text":"Revenue by close month","type":"text","marks":[{"type":"strong"}]},{"text":" (won deals):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"hubspot objects search --type deals \\\n --filter \"hs_is_closed_won=true AND closedate>=2026-01-01\" \\\n --properties amount,closedate \\\n| jq -rs '\n group_by(.properties.closedate[0:7])\n | map({month: .[0].properties.closedate[0:7], count: length,\n revenue: ([.[].properties.amount | select(. != null and . != \"\") | tonumber] | add // 0 | round)})\n | sort_by(.month) | .[] | \"\\(.month)\\tdeals: \\(.count)\\trevenue: $\\(.revenue)\"' \\\n| column -t -s

Source of truth is authoritative. Build on — JSONL shape, batch-read rules, and pagination live there. Reshape patterns: . / cap at 100 rows per call; a result of exactly 100 is almost always truncated — paginate via before aggregating. Property and output shape notes - All CRM property values come back as strings in JSONL — booleans included. is returned as / (string); is a numeric string. Use for arithmetic; compare booleans as strings ( ) when filtering client-side. - Numeric properties can be or an empty string ( ) when unset/blank. aborts on . Always guard with . - In expressions, and wo…

\\t'","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"4. Saved reports","type":"text"}]},{"type":"paragraph","content":[{"text":"The ","type":"text"},{"text":"hubspot reports","type":"text","marks":[{"type":"code_inline"}]},{"text":" family runs HubSpot's saved reports and CRM-SQL reports server-side, so you don't have to recompute aggregates client-side:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"reports list","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get \u003cid>","type":"text","marks":[{"type":"code_inline"}]},{"text":" — browse and inspect saved reports.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"reports fetch-dataset \u003cid>","type":"text","marks":[{"type":"code_inline"}]},{"text":" — re-execute a saved report server-side and return its dataset.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"reports create \"\u003cCRM SQL>\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" — create a report from a CRM-SQL query (a companion skill can generate the SQL); ","type":"text"},{"text":"reports insights \u003cid>","type":"text","marks":[{"type":"code_inline"}]},{"text":" generates AI insights (async, polls to completion).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"reports update \u003cid>","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"clone \u003cid>","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"favorite \u003cid>","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"unfavorite \u003cid>","type":"text","marks":[{"type":"code_inline"}]},{"text":" — manage report metadata.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"reports delete \u003cid>","type":"text","marks":[{"type":"code_inline"}]},{"text":" — irreversible, digest-gated: ","type":"text"},{"text":"--dry-run","type":"text","marks":[{"type":"code_inline"}]},{"text":" first, then ","type":"text"},{"text":"--digest \u003chash> --confirm \"\u003creport name>\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" (confirm = the report's name).","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"hubspot reports --help","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the full surface and the CRM-SQL grammar.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"5. Win/loss context: sequence enrollments","type":"text"}]},{"type":"paragraph","content":[{"text":"hubspot sequences enrollments \u003ccontact_id>","type":"text","marks":[{"type":"code_inline"}]},{"text":" returns a contact's Sales Hub sequence enrollment history (read-only) — useful for attributing wins/losses to outreach. Each row: ","type":"text"},{"text":"{\"contactId\":789,\"enrollments\":[{\"sequenceName\":\"Q4 Outbound\",\"state\":\"FINISHED\",\"enrolledAt\":\"...\",\"currentStepOrder\":5,\"totalSteps\":6}]}","type":"text","marks":[{"type":"code_inline"}]},{"text":". Join it to a deal's associated contacts for a \"was this deal worked through a sequence?\" view. See ","type":"text"},{"text":"hubspot sequences enrollments --help","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Known limitations","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Won/lost stages are identifiable from ","type":"text"},{"text":"hubspot pipelines stages","type":"text","marks":[{"type":"code_inline"}]},{"text":": each stage's ","type":"text"},{"text":"metadata","type":"text","marks":[{"type":"code_inline"}]},{"text":" carries ","type":"text"},{"text":"isClosed","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"probability","type":"text","marks":[{"type":"code_inline"}]},{"text":" (e.g. ","type":"text"},{"text":"jq -r 'select(.metadata.probability==\"1.0\") | .id'","type":"text","marks":[{"type":"code_inline"}]},{"text":"). ","type":"text"},{"text":"hs_is_closed_won","type":"text","marks":[{"type":"code_inline"}]},{"text":" on the deal itself also works.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No team object — group by ","type":"text"},{"text":"hubspot_owner_id","type":"text","marks":[{"type":"code_inline"}]},{"text":" and resolve names from ","type":"text"},{"text":"hubspot owners list","type":"text","marks":[{"type":"code_inline"}]},{"text":" client-side.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"hubspot pipelines","type":"text","marks":[{"type":"code_inline"}]},{"text":" is app-token-only and 403s under user OAuth. ","type":"text"},{"text":"hubspot owners list","type":"text","marks":[{"type":"code_inline"}]},{"text":" works under user OAuth. Keep raw IDs + warn; do not fail the report when pipelines is unavailable.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Numeric CRM properties can be ","type":"text"},{"text":"null","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"\"\"","type":"text","marks":[{"type":"code_inline"}]},{"text":"; always guard ","type":"text"},{"text":"tonumber","type":"text","marks":[{"type":"code_inline"}]},{"text":" with ","type":"text"},{"text":"select(. != null and . != \"\")","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]}]},"metadata":{"date":"2026-11-02","name":"sales-reporting","author":"@skillopedia","source":{"stars":28,"repo_name":"agent-cli-skills","origin_url":"https://github.com/HubSpot/agent-cli-skills/blob/HEAD/sales-reporting/SKILL.md","repo_owner":"HubSpot","body_sha256":"5fd9d63d88e3234c10c2e5753b360762b032266c8688569f978ccbb7dd4eaf70","cluster_key":"9de76f713dcabac1ba0fde0c139884d3100ee8bb56fb0e2e2505034e4d133d33","clean_bundle":{"format":"clean-skill-bundle-v1","source":"HubSpot/agent-cli-skills/sales-reporting/SKILL.md","text_bytes":0,"source_commit":"b310eb544776345b512163e1db99badf712068d0","skills_sh_rank":9242,"attachment_count":0,"text_attachments":0,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"sales-reporting/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"business-sales-marketing","category_label":"Business"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"business-sales-marketing","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"],"import_tag":"skills-sh-top20k-2026-11","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."}},"renderedAt":1793661657369}

Source of truth is authoritative. Build on — JSONL shape, batch-read rules, and pagination live there. Reshape patterns: . / cap at 100 rows per call; a result of exactly 100 is almost always truncated — paginate via before aggregating. Property and output shape notes - All CRM property values come back as strings in JSONL — booleans included. is returned as / (string); is a numeric string. Use for arithmetic; compare booleans as strings ( ) when filtering client-side. - Numeric properties can be or an empty string ( ) when unset/blank. aborts on . Always guard with . - In expressions, and wo…