# Reading a HubSpot portal without changing it

Routes in order of preference, what each one shows, and the counting recipes. Checked on 2026-11-06 against HubSpot's knowledge base and developer docs. Menu labels, entitlements and tool names change: verify each path at run time and cite the page used.

## Baseline first

- Confirm the portal is the one the user named (not an old agency portal or a test account) and record its placeholder label, edition, timezone and currency.
- Run the identity check before any MCP or CLI read: `get_user_details` on the remote MCP server (it returns the user and read and write access per object), or `hubspot whoami` in the Agent CLI. Write down whether the identity can write.
- If it can write, list every write tool by name and ask the user to set each to ask or deny in the client (the agent does not change its own permissions). Wait for their confirmation before the first read, and record the denied tools in the report baseline.

## 1. Admin exports, no credentials shared

The admin exports; the agent reads files. This is the safest route and works on any edition.

- Export all properties (in Settings, under Properties; needs the export permission). Choose CSV.
  - One file per object, zipped when several objects are chosen.
  - It holds name, internal name, type, description, group, options, creator, usages and fill rate, plus last updated time and update source for contact, company, deal and ticket properties.
  - Header names are not fixed across versions: `normalize_exports.py hubspot-properties` maps them by alias, prints its mapping, and accepts `--map key=Header`.
  - The export has no created date. Take `createdAt` from `get_properties` or the properties API when FLD-05 needs it; otherwise the script ages fields by last updated time.
- Export property definition for each critical property (More > Export property definition): a ZIP of three CSVs, Access, Property information, and Usage in HubSpot tools with tool type, name and edit URL. This is the dependency evidence before any archive.
- Property history for each critical field, for `writer_history.py`: Settings > Properties > choose the object > hover the property > More > Export property history, with Include Source Information ticked. Without that box the file names no writer and the script stops. It needs CRM Export and [Object] Edit All, is on all plans, and gives one file per property with dates in UTC.
  - The file is wide: one row per record with a Record ID column, then one group of columns per revision, newest first, left to right. HubSpot documents no other header, so print the header row only, then pass `--field`, `--object` and `--group` with one revision's columns in order (`value,time,source`, or `skip` for a column to ignore). The script stops without `--group`, because a wide file read as one change per row shows at most the oldest revision.
  - Caps: 45 revisions per contact property and 20 per company, deal, ticket or custom object property; the cap for leads is not documented. The script notes records at the cap, because their older changes are gone.
  - Each change has one source text (the user or tool) in a format HubSpot does not document, and it can hold a user's address. Run `writer_history.py <file> --format hubspot --field <property> --object <object> --group value,time,source --list-sources sources.csv`, which prints a count only. The user fills the label column with `type:name` labels, and that file is the `--writer-map`. An unmapped source counts as automated and shows as a short hash.
  - A long file, one change per row (from `propertiesWithHistory` reads or the Agent CLI), is matched by column alias. Otherwise convert it to the generic CSV (object, record_id, field, changed_at, value, writer).
  - Workflows and apps often appear by numeric ID. Map every workflow and app ID in the history to its inventory label (`type:name`, per [inventory-format.md](inventory-format.md)) in a writer map and pass `--writer-map writers.csv` before reading FLD-02, or one workflow counts as two writers. `audit_crm.py` also matches `workflow:<id>` to the automation with that `id`.
  - For identity fields (email, phone, name), ask for the value column replaced by a one-way hash or a value token before export, or leave them out of the writer sample. The script only compares values for equality.
- Settings captures (screenshots or copied values): company automation (create and associate companies with contacts, verify the path), the Lifecycle Stage tab automation toggles, form lifecycle and marketing contact settings, the Salesforce integration settings and Sync Health, the Limits tab, connected apps, legacy private apps, service keys, users with the Super Admin flag, security settings (two-factor, SSO), account cleanup.
- Entitlements that change what you can see: the property tabs Usages, Data quality, Data sources and Audit need Data Hub Professional or Enterprise; the property audit log needs Enterprise; viewing audit logs needs Super Admin; account activity history is available from Starter, with data by subscription.

## 2. Remote HubSpot MCP server, read tools only

- Endpoint `https://mcp.hubspot.com`. It acts as the signed-in user, so its reach equals that user's permissions. With Sensitive Data on, activities and conversations are blocked through MCP.
- Read tools for this audit: `get_user_details` (first), `get_organization_details` (account type, seats, timezone, currency), `discover_hubspot_schema`, `search_properties`, `get_properties`, `search_owners` (100 per call), `search_crm_objects` (200 per page, up to five filter groups of six filters), `get_crm_objects` (up to 100 IDs), `query_crm_data` for aggregate counts (call `tool_guidance` first; it aggregates but cannot join or traverse associations, so use association count properties).
- Allow only the read tools listed here and set every other tool to deny: every `manage_*` tool (records, segments, properties, pipelines, campaigns, marketing email, landing and website pages, blog posts, AEO prompts and recommendations, onboarding; several can publish) and `submit_feedback`, which sends data to HubSpot. The server lists 32 tools today and new ones appear, so an allowlist stays safe where a denylist goes stale.
- Always pass an explicit properties list to `search_crm_objects` and `get_crm_objects`: `hs_object_id` plus the fields under audit, never `email`, `firstname`, `lastname` or `phone`. The CRM API returns `email`, `firstname` and `lastname` for contacts when no list is passed, and the MCP page does not say what these tools do.
- State whether more pages remain. A first page is never the whole portal.

## 3. HubSpot Agent CLI (beta), read commands only

- The user logs in with OAuth (`hubspot auth login`); access mirrors their permissions. Never use admin mode with a service key: it has no per-user audit trail.
- Read commands: `hubspot whoami`, `properties list`, `pipelines list`, `pipelines stages`, `associations labels-list`, `associations limits-list`, `owners list`, `workflows list`, `workflows get`, and `objects history --type <object> --properties <property> --include-ui` (named in the agent-cli-skills bulk-operations skill, not on the CLI guide). Confirm names with `--help`; the CLI is beta.
- `workflows list` is for collecting flow IDs. Treat its output as summaries and never normalize it: the v4 list endpoint returns key fields only (such as id, isEnabled, objectTypeId and revisionId), with no actions or enrolment criteria.
- It reads v4 flows only, 100 per call (`--after`). Classic contact workflows from `/automation/v3/workflows` are not returned, so compare the count with the Workflows page. Enter each missing workflow in the inventory by name: with nothing recorded it keeps the AUTO rules NOT CHECKED and the automation verdict Not assessable, and once it is read from a screenshot its three `_known` flags go true ([inventory-format.md](inventory-format.md)).
- `workflows get <flowId>` fetches one or more workflows by ID in the v4 shape; save the output and run `normalize_exports.py hubspot-flows` on the file. Check that each flow carries an actions list: the script keeps a flow without one as unread (writes, actions and references unknown) and names it, so AUTO-03, AUTO-06 and the other rules that need it cannot pass.
- Never run create, update, delete, merge, batch or schema commands during the audit.

## 4. Workflows v4 API (beta)

Any v4 endpoint needs the `automation` scope, which also allows writes. Prefer the CLI under the user's login or a UI capture. Workflows deleted through the API cannot be restored through the API.

- `GET /automation/v4/flows` returns key fields only; `GET /automation/v4/flows/{flowId}` returns the full definition with actions and enrollmentCriteria.
- `POST /automation/v4/flows/batch/read` fetches several workflows by ID. The guide shows no response example, so check that its output carries actions before normalizing it.

## 5. Counting recipes

Confirm every internal name with `get_properties` before counting. Each count records numerator, denominator, population, query and read time.

- Open deals without a company (OBJ-04): open deals with the associated company count at 0, over all open deals.
- Customer without evidence (LIFE-06): `lifecyclestage` = customer with "Recent deal close date" unknown and no billing ID, over all Customer contacts.
- Unowned routing population (FLD-08): sales-eligible contacts with `hubspot_owner_id` unknown.
- Past close dates (REP-05): open deals with `closedate` before the read date.
- Unknown source (REP-06): contacts created in 90 days with Original Traffic Source offline or unknown.
- Stage completeness (PIPE-04): deals in each stage with each required property unknown.
- Fill rate for a population: the property export's fill rate covers all records, so recount with a filter for the rule's population.

## 6. HubSpot behaviours that shape findings

- Companies created through the API, sync apps included, are not deduplicated by domain.
- Imports, forms, the API, workflows and the Salesforce integration only move lifecycle forward. Going back needs the value cleared first; a workflow clear-then-set needs Professional or Enterprise.
- For a new record from a form, the form's lifecycle setting wins over the default for new records.
- Conditional stage properties are documented for manual creates and moves in the UI only.
- A deal stage's probability decides whether it is closed: 100% is closed won and 0% closed lost. A deal's close date is set when it enters a closed stage and is not updated when it moves from one closed won stage to another.
- Property view and edit restrictions and Sensitive Data need Enterprise. Super Admins see every property, and any user can set a restricted property through the API or when creating a record.
- Data sync filters limit only the initial sync. The default app wins discrepancies within the configured direction. Owner mapping with Salesforce is two-way, and a field's first sync takes the Salesforce value as the baseline.
- Smart Transfer and the native Salesforce integration match on different identifiers; running both creates duplicates.
- Merges create a new record ID unless the primary ID preservation beta is on. Records cannot be unmerged.
- Archived properties restore for 90 days, then are deleted. Deleted workflows restore for 90 days, as new inactive workflows without history. Enterprise account cleanup (beta) can remove unused workflows automatically.
- Restore CRM changes rolls back a named import, integration or workflow for 14 days and needs a Super Admin.
- Legacy private app creation ended on 28 September 2026 for new accounts and 26 October 2026 for existing accounts; existing apps keep working, and service keys from developer projects replace them.
- Usage-based features (Data Agent, Prospecting Agent, Breeze actions in workflows) pause when credits run out and no capacity pack was bought, or when the account spend limit is reached. Data enrichment does not consume credits; smart properties do.
- Workflow action type IDs the scripts classify are listed in [rules-process.md](rules-process.md).
