Team Ownership

hubspot/agent-cli-skills/team-ownership

by hubspota8eea0880838No license27 starsListed Oct 8, 2026Updated Oct 8, 2026Repository updated 7 days ago

Assign and reassign CRM record ownership, audit who-owns-what across object types, and handle rep transitions. Built on `bulk-operations`.

Instructions onlyBusiness & Finance
AI-generated overview

Assign, reassign and audit CRM record ownership across contacts, companies, deals and tickets using the hubspot CLI.

What it does
This skill provides instructions for assigning and reassigning ownership of CRM records, auditing which users own which records across object types, and handling representative transitions. It covers resolving owner emails to portal-specific IDs, searching records by owner or for unowned records, and performing bulk or single-record reassignments with dry-run, digest and confirm safeguards. It also describes client-side team-level views by grouping records by owner ID and joining them to the owners list.
When to use it
Use it when records need a new owner, when a sales representative is leaving and their records must be transferred, or when you need to audit ownership across contacts, companies, deals and tickets. It is also suited to redistributing accounts or finding records owned by a specific person.
Requirements
Requires the hubspot CLI with objects, owners and history commands, plus jq for JSON reshaping and shell access for piping. It depends on the bulk-operations skill for JSONL piping, pagination, dry-run/digest/confirm and history recovery. No scripts are shipped; it is instructions only.

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:

bash
FROM_ID=$(hubspot owners list | jq -r 'select(.email=="[email protected]") | .id')TO_ID=$(hubspot owners list | jq -r 'select(.email=="[email protected]")  | .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.

bash
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 --after loop from bulk-operations. Counting only: pipe to wc -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").

bash
# 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 count
hubspot 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):

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

bash
FROM_ID=$(hubspot owners list | jq -r 'select(.email=="[email protected]")    | .id')TO_ID=$(hubspot  owners list | jq -r 'select(.email=="[email protected]") | .id')
for type in contacts companies deals tickets; do  echo "── $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:

bash
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.json
hubspot owners list \| jq --slurpfile by /tmp/by-owner.json -r \     '. as $o | $by[0][] | select(.owner_id==$o.id) | "\($o.email)\t\(.count)"'

Source and attribution

Source:hubspot/agent-cli-skillsinteam-ownershipat commita8eea08

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal