Skill v1.0.1
currentAutomated scan100/100+1 new
version: "1.0.1" name: team-ownership description: Assign and reassign CRM record ownership, audit who-owns-what across object types, and handle rep transitions. Built on bulk-operations. triggers:
- "assign owner"
- "reassign records"
- "ownership audit"
- "rep leaving"
- "transfer records"
- "change owner"
- "find records owned by"
- "redistribute accounts"
Prereq: read bulk-operations/SKILL.md first. JSONL piping, pagination, dry-run/digest/confirm, and hubspot history recovery live there. Reshape patterns live in bulk-operations/resources/json-patterns.md.
hubspot_owner_id is a string field on contacts, companies, deals, and tickets. Owners are CRM users — hubspot owners list returns them; there is no teams object, so team-level views are client-side groupings by hubspot_owner_id.
1. Resolve email → owner ID
Never hardcode IDs — they are portal-specific. Resolve, then cache:
FROM_ID=$(hubspot owners list | jq -r 'select(.email=="sarah@company.com") | .id')TO_ID=$(hubspot owners list | jq -r 'select(.email=="mike@company.com") | .id')
2. Find records for an owner
Same filter across all four object types. Add object-specific --properties for context. Unowned records use the !property form.
hubspot objects search --type contacts --filter "hubspot_owner_id=$FROM_ID" --properties email,firstname,lifecyclestagehubspot objects search --type companies --filter "hubspot_owner_id=$FROM_ID" --properties name,domainhubspot objects search --type deals --filter "hubspot_owner_id=$FROM_ID" --properties dealname,dealstage,amounthubspot objects search --type tickets --filter "hubspot_owner_id=$FROM_ID" --properties subject,hs_pipeline_stage# Records with no owner at allhubspot objects search --type deals --filter "!hubspot_owner_id" --properties dealname,amount
100 hits — page with the--afterloop frombulk-operations. Counting only: pipe towc -l.
3. Bulk reassign — search → update
Reshape each search row into {id, properties:{hubspot_owner_id}} and pipe to objects update. objects update is irreversible, so dry-run first: the dry-run emits a digest + apply_command_hint at every size (confirm = the record ID for one record, the row count for a batch). Then re-run with --digest/--confirm (see bulk-operations/SKILL.md § "Safe destructive workflow").
# Dry-runhubspot objects search --type contacts --filter "hubspot_owner_id=$FROM_ID" \| jq -c --arg to "$TO_ID" '{id, properties:{hubspot_owner_id:$to}}' \| hubspot objects update --type contacts --dry-run \| tee /tmp/reassign.preview.jsonl# Execute — lift the digest + confirm from the preview line (required at every size)digest=$(jq -r 'select(.digest != null) | .digest' /tmp/reassign.preview.jsonl)confirm=$(jq -r 'select(.digest != null) | .target.id' /tmp/reassign.preview.jsonl) # single: record ID; batch: row counthubspot objects search --type contacts --filter "hubspot_owner_id=$FROM_ID" \| jq -c --arg to "$TO_ID" '{id, properties:{hubspot_owner_id:$to}}' \| hubspot objects update --type contacts --digest "$digest" --confirm "$confirm"
Single-record assignment — no stdin, no jq. Still irreversible, so dry-run first (confirm = the record ID):
hubspot objects update --type contacts 12345 --property hubspot_owner_id=$TO_ID --dry-runhubspot objects update --type contacts 12345 --property hubspot_owner_id=$TO_ID --digest <hash> --confirm 12345
4. Rep-leaves workflow
Loop over the four object types the rep touches:
FROM_ID=$(hubspot owners list | jq -r 'select(.email=="leaving@company.com") | .id')TO_ID=$(hubspot owners list | jq -r 'select(.email=="taking-over@company.com") | .id')for type in contacts companies deals tickets; doecho "── $type ──"hubspot objects search --type "$type" --filter "hubspot_owner_id=$FROM_ID" \| jq -c --arg to "$TO_ID" '{id, properties:{hubspot_owner_id:$to}}' \| hubspot objects update --type "$type" --dry-rundone
Review each digest line, then re-run per type with the --digest/--confirm lifted from that type's preview line (required at every size; confirm = the row count for a batch, the record ID for a single). Mis-reassigned? hubspot history --since 1h lists the affected IDs.
5. Team-level views (client-side grouping)
Group records by hubspot_owner_id, join to owners list for human-readable emails:
hubspot objects search --type deals --filter "dealstage!=closedwon AND dealstage!=closedlost" \--properties hubspot_owner_id --format json \| jq '.data | group_by(.properties.hubspot_owner_id)| map({owner_id: .[0].properties.hubspot_owner_id, count: length})' \> /tmp/by-owner.jsonhubspot owners list \| jq --slurpfile by /tmp/by-owner.json -r \'. as $o | $by[0][] | select(.owner_id==$o.id) | "\($o.email)\t\(.count)"'