# CRM

<!-- BEGIN GENERATED: guide-map -->
## Guide map

Read only the section needed for the next decision. Line ranges are exact
for this generated runtime target.

| Section | What it covers | Lines |
| --- | --- | --- |
| [Purpose](#purpose) | Scope, ownership, and the central operating question. | 29-40 |
| [Primary SDK Paths](#primary-sdk-paths) | Operating guidance and contracts for this section. | 41-56 |
| [Supporting References](#supporting-references) | Capability, provider, policy, and runtime guides for this outcome. | 57-64 |
| [Use This When](#use-this-when) | Tasks and conditions that belong in this guide. | 65-84 |
| [Do Not Use This For](#do-not-use-this-for) | Boundaries and the correct routes for excluded work. | 85-102 |
| [Choose A Path](#choose-a-path) | Decision hierarchy for selecting the right capability or provider. | 103-116 |
| [Mental Model](#mental-model) | Concepts and state boundaries needed for correct operation. | 117-134 |
| [Preflight](#preflight) | State, authority, inputs, and readiness to verify before execution. | 135-169 |
| [Risk And Side Effects](#risk-and-side-effects) | Effects, approvals, cost, privacy, and retry boundaries. | 170-187 |
| [Canonical Operating Pattern](#canonical-operating-pattern) | The normal end-to-end execution sequence. | 188-218 |
| [Core SDK Surfaces](#core-sdk-surfaces) | Exact source-backed callable contracts owned by this guide. | 219-239 |
| [Data Contracts](#data-contracts) | Required identities, fields, evidence, handles, and durable shapes. | 240-261 |
| [Examples](#examples) | Representative usage patterns and code. | 262-450 |
| [Verification](#verification) | Evidence required before reporting success. | 451-470 |
| [Troubleshooting](#troubleshooting) | Failure diagnosis and corrective routes. | 471-490 |
| [Related References](#related-references) | Adjacent boundaries and when to cross them. | 491-503 |
<!-- END GENERATED: guide-map -->

## Purpose

Use `vf.crm.*` to discover and manage HubSpot lists, read CRM provider schema,
fetch CRM object rows, fetch associations, and run receipt-backed contact or
company upserts through Bridge. This reference owns CRM facts that must
be known before audience building, suppression, routing, enrichment, workflow
source filters, or sequence staging.

The current Bridge-backed CRM surface is HubSpot-focused. Treat `contacts`,
`companies`, and `deals` as the supported object types unless the active SDK
and backend contract explicitly show more.

## Primary SDK Paths

<!-- BEGIN GENERATED: primary-sdk-paths -->
| Primary function | Choose it when | Required inputs | Returns | Exact contract |
| --- | --- | --- | --- | --- |
| `vf.integrations.status` | Use first to verify the requested CRM provider is connected for the current workspace. | `provider` | `integration_status` | [`auth.md`](auth.md) · installed SDK: `verbiflow/integrations.py:28-42` |
| `vf.crm.list_lists` | Use to search HubSpot lists and capture a stable list_id before fetching its current membership. | None | `crm_lists` | [`crm.md`](crm.md) · installed SDK: `verbiflow/crm.py:67-118` |
| `vf.crm.create_list` | Use to create a new MANUAL HubSpot list and preserve its provider list_id for later membership writes. | `name`, `object_type` | `crm_list_create_receipt` | [`crm.md`](crm.md) · installed SDK: `verbiflow/crm.py:120-162` |
| `vf.crm.upsert_to_list` | Use to resolve contacts or companies from one or more typed identifiers, fail closed on identity conflicts, upsert by a proven unique property, and ensure successful records are members of a writable HubSpot list. | `list_id`, `object_type`, `records` | `pending_artifact_job` | [`crm.md`](crm.md) · installed SDK: `verbiflow/crm.py:204-289` |
| `vf.crm.delete_list` | Use only after explicit confirmation to delete a HubSpot list; HubSpot permits restoration for 90 days. | `list_id` | `crm_list_delete_receipt` | [`crm.md`](crm.md) · installed SDK: `verbiflow/crm.py:164-202` |
| `vf.crm.list_properties` | Use before querying to discover canonical provider property names and types for one CRM object. | `object_type` | `crm_properties` | [`crm.md`](crm.md) · installed SDK: `verbiflow/crm.py:42-65` |
| `vf.crm.fetch_objects` | Use to create a durable artifact from a HubSpot list or from explicit provider filters and selected properties. | `object_type` | `pending_artifact_job` | [`crm.md`](crm.md) · installed SDK: `verbiflow/crm.py:537-617` |
| `vf.crm.fetch_associations` | Use after object retrieval when relationship edges such as contact-company or company-deal are required. | `from_object_ids`, `from_object_type`, `to_object_type` | `pending_artifact_job` | [`crm.md`](crm.md) · installed SDK: `verbiflow/crm.py:619-658` |
| `vf.crm.enrich_contacts` | Use when known match values such as email addresses need selected HubSpot contact and associated-company properties inline. | `match_values` | `crm_contact_enrichment_rows` | [`crm.md`](crm.md) · installed SDK: `verbiflow/crm.py:761-840` |
<!-- END GENERATED: primary-sdk-paths -->

## Supporting References

Open only the child that resolves the next material decision.

| Reference | Use it for | Kind |
| --- | --- | --- |
| [`auth.md`](auth.md) | Runtime authority, provider integration readiness, and user-owned connection flows. | runtime |

## Use This When

- You need to know the real CRM property name before writing a filter or
  selecting fields.
- You need HubSpot contact, company, or deal rows as a durable artifact.
- You need to discover an existing HubSpot list and acquire its current
  membership without rebuilding the list's criteria as CRM property filters.
- You need company-contact, company-deal, or contact-company associations for
  suppression, ownership, routing, expansion, or audit output.
- A workflow depends on active-customer flags, deal association, lifecycle
  stage, owner, territory, segment, or CRM-created/contact-created windows.
- You already have match values (usually emails) and need selected HubSpot
  contact and associated-company property values inline, without an artifact
  job. Use `vf.crm.enrich_contacts(...)`.
- You need to prove why a row was included, excluded, routed, or staged based
  on CRM state.
- You need to upsert contacts or companies from several known identities, such
  as email plus LinkedIn URL or domain plus LinkedIn company URL, without
  creating a duplicate when those identifiers disagree.

## Do Not Use This For

- General audience discovery outside CRM. Use `audience_discovery.md`,
  `linkedin.md`, `web.md`, `places.md`, `clay.md`, or `crunchbase.md`.
- Email, phone, company, domain, LinkedIn, or identity enrichment. Use
  `enrichment.md`.
- Local row shaping after CRM artifacts are fetched. Use `row_operations.md`
  and `artifacts_jobs.md`.
- Sequence source authoring, source staging, publish, sends, or source repair.
  Use `sequence_authoring.md`, `sequences.md`, and `safety.md`.
- Workflow checkpointing and source-window semantics. Use
  `workflow_sources.md`; CRM only owns the provider request shape.
- General CRM record mutation. The only record write surface here is the
  constrained `upsert_to_list` operation for contacts and companies.
- Sequence destination sync configuration, manual runs, row status, or provider
  writes from a sequence Audience. Use `sequences.md` for routing and
  `docs/destination-sync.md` for the operating contract.

## Choose A Path

1. Check provider readiness before any schema or object request.
2. Discover lists with `vf.crm.list_lists(...)` when HubSpot already owns the
   intended audience definition. Preserve the returned `list_id`.
3. Read schema first when a filter, property, pipeline stage, or association is
   not already proven in the current run.
4. Use object fetches for bounded CRM rows and association fetches for graph
   facts. Preserve each job and artifact separately.
5. Use inline contact enrichment only when you already have match values and
   need a small selected property set immediately.
6. Perform suppression, routing, and qualification as visible downstream row
   operations. CRM acquisition does not silently activate a sequence.

## Mental Model

CRM work is schema-first and artifact-backed:

1. Confirm the workspace and connected CRM provider.
2. Either search HubSpot lists and select a stable list ID, or read properties,
   pipelines/stages, or association types before assuming
   provider field names or graph semantics.
3. Start CRM object or association fetches as Bridge artifact jobs.
4. Wait/page/export the resulting artifact using `vf.jobs.result(...)` and
   `artifacts_jobs.md`.
5. Join/filter those CRM facts visibly before enrichment, qualification,
   reporting, sequence staging, or sends.

CRM fetches are data inputs. They do not suppress, route, stage, publish, or
send by themselves. Suppression is the explicit join/filter step you perform
after fetching the relevant CRM facts.

## Preflight

1. Confirm the active SDK profile and workspace with `auth.md`.
2. Check CRM readiness with `vf.crm.status(provider="hubspot")`. If debugging
   integration state directly, `vf.integrations.status("hubspot")` is an auth
   readiness surface; CRM still owns the schema/object work after readiness.
3. Call `vf.crm.list_properties(...)` before using a property in filters or
   selected columns. Do not infer internal property names from display labels
   such as "Active Customer" or "Product User".
4. For deal suppression, call `vf.crm.list_pipelines(...)` or
   `vf.crm.list_deal_stages(...)` before relying on stage ids, archived flags,
   or closed/open semantics.
5. For association work, call `vf.crm.list_association_types(...)` when the
   default association is not enough or the provider requires a label/type.
6. Bound row volume and selected fields before fetches. Object fetches default
   to the backend row limit when `limit` is omitted; request only fields you
   need and preserve CRM object IDs.
7. `list_id` and `filters` are mutually exclusive. A selected list must contain
   the same `object_type` requested by `fetch_objects`.
8. Reauthorization must use the same HubSpot portal already bound to the
   workspace. Disconnect removes credentials, not CRM source, exclusion, or
   destination-sync configuration; reconnecting the same portal restores
   provider access without moving those configurations to another account.
9. When CRM facts define exclusion, suppression, deduplication, routing, or
   membership, the CRM read is a required semantic gate. Require a connected
   provider, successful complete object pages, and successful complete
   association jobs. Do not catch an error, substitute an empty set, and
   continue as though no matches exist. A failed or partial gate must fail the
   step before enrichment, scoring, staging, or publication.
10. Before a resolution-aware upsert, inspect `hasUniqueValue` from
   `vf.crm.list_properties(...)`. Standard LinkedIn display properties are
   matchable but are not automatically unique create identifiers. Provision a
   dedicated normalized unique property before creating email-less contacts or
   domain-less companies from LinkedIn identity.

## Risk And Side Effects

| Operation | Risk | Rule |
| --- | --- | --- |
| CRM status/list/properties/pipelines/association-type reads | Platform/provider read through Bridge policy | Safe as reads, but still workspace-scoped provider calls. Print provider/workspace when it affects correctness. |
| `vf.crm.fetch_objects(...)` | Bridge artifact job and HubSpot/provider read | Preserve `job_id` and `artifact_id`; inspect rows before downstream use. |
| `vf.crm.fetch_associations(...)` / `_all(...)` | Bridge artifact job and HubSpot/provider read | Input IDs must be CRM object IDs, not job/artifact IDs. Chunk large sets and preserve every chunk job. |
| `vf.crm.create_list(...)` | Provider write | Creates a `MANUAL` HubSpot list. Bridge reconciles an ambiguous response by HubSpot's unique list name. Preserve the returned provider `list_id`. |
| `vf.crm.upsert_to_list(...)` | Provider reads/writes plus durable receipt artifact | Preflights the target list and properties. Resolution-aware rows read every supplied identifier first, update only when all matches converge, and fail closed on ambiguity or conflict. Creation requires email/domain or another provider-declared unique property. Inspect identity, upsert, and membership states; `unknown` is not failure. |
| `vf.crm.ensure_associations(...)` | Provider reads/writes plus durable per-pair receipt artifact | Call only after both CRM record IDs are resolved from successful upsert/fetch receipts. Use one digest-guarded idempotency key per logical pair set. The provider edge is read before creation, so repeats converge; inspect every pair and reconcile `unknown` rather than guessing. |
| `vf.crm.delete_list(...)` | Destructive provider write | Deletes a HubSpot list. HubSpot retains deleted lists for restoration for 90 days. Workflow Plays must require explicit human confirmation immediately before this call. |
| Local suppression/routing joins | Local or artifact transform | Keep audit outputs for both kept and excluded rows when suppression affects outreach. |
| Sequence staging/publish/send | Out of scope mutation | CRM facts may justify the action, but sequence references own the mutation and approval gates. |

CRM capabilities currently use backend Growth policy/audit and no-credit
capability policy, but agents should still treat fetches as provider-backed
jobs that can be slow, fail, paginate, and produce partial or stale data.

## Canonical Operating Pattern

1. Print the active workspace and CRM connection status.
2. Choose exactly one acquisition definition:
   - call `vf.crm.list_lists(...)` and preserve `list_id` when HubSpot already
     owns the audience definition; or
   - discover schema and construct explicit property filters.
3. Discover schema when using property filters:
   - properties for the target object type;
   - pipelines/deal stages for deal logic;
   - association types for non-default graph edges.
4. Start a bounded object fetch with explicit `object_type`, `properties`,
   either `filters` or `list_id`, `limit`, `provider`, and `artifact_label`.
5. Wrap the returned `PendingJob` with `vf.jobs.result(job)` and wait/page rows
   using `artifacts_jobs.md`.
6. For associations, collect CRM object IDs from fetched rows, then use
   `vf.crm.fetch_associations(...)` for one bounded set or
   `vf.crm.fetch_associations_all(...)` for chunked sets.
   Start every chunk through that helper and require every returned job to
   succeed before treating the association set as complete; never serialize
   per-company waits or swallow failed chunks.
7. Join CRM objects and associations through artifact joins or local row
   operations. Keep suppression/routing columns and rejected-row audit outputs.
8. Only after the CRM join is visible should you enrich, qualify, report, stage
   to a sequence, or publish/send.
9. For activation, pass a stable `source_key`, every known typed identifier,
   and the intended properties to `vf.crm.upsert_to_list(...)`. Treat
   `ambiguous`, `conflict`, `unresolvable`, and `unknown` receipts as held rows.
   Use resolved provider IDs from successful company and contact receipts for
   `vf.crm.ensure_associations(...)`.

## Core SDK Surfaces

<!-- BEGIN GENERATED: core-sdk-surfaces -->
| Function | Use When | Inputs | Output And Handles To Preserve | Side Effect | Source |
| --- | --- | --- | --- | --- | --- |
| `vf.crm.create_list` | Use when reading CRM metadata or starting CRM object/association fetch jobs through Bridge. | <strong>Required</strong>: `name` (str; required): Name for the new manual list.; `object_type` (contacts \| companies \| deals; required): CRM object type. Current Bridge-backed CRM support is `contacts`, `companies`, and `deals`.<br><strong>Optional</strong>: `provider` (hubspot; optional; default `'hubspot'`): CRM provider. Current Bridge-backed CRM support is HubSpot (`hubspot`). | `crm_list_create_receipt` payload returned by the SDK operation. Inspect the documented function reference and `raw`/dictionary fields when debugging provider-specific data.<br><strong>Common next calls</strong>: `vf.crm.upsert_to_list` | provider_call | installed SDK: `verbiflow/crm.py:120-162` |
| `vf.crm.delete_list` | Use when reading CRM metadata or starting CRM object/association fetch jobs through Bridge. | <strong>Required</strong>: `list_id` (str; required): Provider list identifier to delete. HubSpot permits restoration for 90 days.<br><strong>Optional</strong>: `provider` (hubspot; optional; default `'hubspot'`): CRM provider. Current Bridge-backed CRM support is HubSpot (`hubspot`). | `crm_list_delete_receipt` payload returned by the SDK operation. Inspect the documented function reference and `raw`/dictionary fields when debugging provider-specific data. | provider_call | installed SDK: `verbiflow/crm.py:164-202` |
| `vf.crm.enrich_contacts` | Use to pull selected HubSpot contact and associated-company property values inline for known match values such as emails. | <strong>Required</strong>: `match_values` (Sequence[str]; required): Values of the match property (usually emails) to look up, at most 500 per call; chunk larger sets across calls.<br><strong>Optional</strong>: `company_properties` (Sequence[str \| JsonObject]; optional): Associated-company properties to return, same item shape as contact_properties.; `contact_properties` (Sequence[str \| JsonObject]; optional): Contact properties to return. Each item is a property name (render raw) or {'name': ..., 'render': 'raw'\|'owner_name'}; 'owner_name' resolves owner-reference ids to owner names and exposes the value as `<property>_name`.; `match_property` (str; optional; default `'email'`): CRM contact property compared against match_values, usually email.; `provider` (str; optional; default `'hubspot'`): CRM provider. Current Bridge-backed CRM support is HubSpot (`hubspot`). | `dict` with one row per match value: `matched`, `crm_contact_id`, `crm_company_id`, and selected `contact`/`company` values (`owner_name` renders keyed as `<property>_name`), plus `matched_count` and `company_matched_count`.<br><strong>Common next calls</strong>: `vf.crm.list_properties` | provider_call | installed SDK: `verbiflow/crm.py:761-840` |
| `vf.crm.ensure_associations` | Use when reading CRM metadata or starting CRM object/association fetch jobs through Bridge. | <strong>Required</strong>: `from_object_type` (contacts \| companies; required): CRM source object type for association fetches. Current Bridge-backed CRM support is `contacts`, `companies`, and `deals`.; `pairs` (Sequence[JsonObject]; required): Unique resolved CRM record pairs with from_id and to_id.; `to_object_type` (contacts \| companies; required): CRM destination object type for association fetches. Current Bridge-backed CRM support is `contacts`, `companies`, and `deals`.<br><strong>Optional</strong>: `artifact_label` (str \| None; optional; default `None`): Human-readable label for the artifact created by the backend job.; `association_category` (HUBSPOT_DEFINED \| USER_DEFINED; optional; default `'HUBSPOT_DEFINED'`): Category reported by crm.list_association_types.; `association_type_id` (int \| None; optional; default `None`): Optional association type returned by crm.list_association_types; omit to resolve the default unlabeled provider-defined type.; `idempotency_key` (str \| None; optional; default `None`): Stable key for this exact logical pair set and request body.; `provider` (str; optional; default `'hubspot'`): CRM provider. Current Bridge-backed CRM support is HubSpot (`hubspot`). | `PendingJob`: `job_id` immediately; wrap with `vf.jobs.result(pending).wait(...)`, then use `.artifact()`, `.rows()`, `.to_report()`, or `.stage_to_sequence(...)` when `artifact_id` is present.<br><strong>Common next calls</strong>: `vf.jobs.result`, `vf.artifacts.inspect` | provider_job | installed SDK: `verbiflow/crm.py:291-408` |
| `vf.crm.fetch_associations` | Use after fetching CRM objects when you need contact-company, company-deal, or other CRM graph edges. | <strong>Required</strong>: `from_object_ids` (list[str]; required): CRM source object ids, not job ids or artifact ids. The SDK deduplicates blank/repeated ids; the backend accepts at most 1000 source ids per association job.; `from_object_type` (str; required): CRM source object type for association fetches. Current Bridge-backed CRM support is `contacts`, `companies`, and `deals`.; `to_object_type` (str; required): CRM destination object type for association fetches. Current Bridge-backed CRM support is `contacts`, `companies`, and `deals`.<br><strong>Optional</strong>: `artifact_label` (str \| None; optional; default `None`): Human-readable label for the artifact created by the backend job.; `association_type` (str \| None; optional; default `None`): CRM association type id/name; omit to use the provider default association between the object types.; `provider` (str \| None; optional; default `None`): CRM provider. Current Bridge-backed CRM support is HubSpot (`hubspot`). | `PendingJob`: `job_id` immediately; wrap with `vf.jobs.result(pending).wait(...)`, then use `.artifact()`, `.rows()`, `.to_report()`, or `.stage_to_sequence(...)` when `artifact_id` is present.<br><strong>Common next calls</strong>: `vf.jobs.result`, `vf.artifacts.inspect` | artifact_job | installed SDK: `verbiflow/crm.py:619-658` |
| `vf.crm.fetch_associations_all` | Use when you have many CRM object ids and need provider-safe chunked association fetch jobs. | <strong>Required</strong>: `from_object_ids` (list[str]; required): CRM source object ids, not job ids or artifact ids. The SDK deduplicates blank/repeated ids; the backend accepts at most 1000 source ids per association job.; `from_object_type` (str; required): CRM source object type for association fetches. Current Bridge-backed CRM support is `contacts`, `companies`, and `deals`.; `to_object_type` (str; required): CRM destination object type for association fetches. Current Bridge-backed CRM support is `contacts`, `companies`, and `deals`.<br><strong>Optional</strong>: `artifact_label` (str \| None; optional; default `None`): Human-readable label for the artifact created by the backend job.; `association_type` (str \| None; optional; default `None`): CRM association type id/name; omit to use the provider default association between the object types.; `chunk_size` (int; optional; default `1000`): Maximum source ids per association fetch job.; `concurrency` (int; optional; default `4`): Maximum association fetch jobs to start or wait concurrently.; `provider` (str \| None; optional; default `None`): CRM provider. Current Bridge-backed CRM support is HubSpot (`hubspot`).; `wait` (bool; optional; default `False`): When true, wait for all association jobs before returning. | `list[PendingJob]`: one handle per started/waited chunk. Use `vf.jobs.wait_many(...)`, then wrap each item with `vf.jobs.result(...)` for rows/artifacts.<br><strong>Common next calls</strong>: `vf.jobs.wait_many`, `vf.jobs.stream_rows`, `vf.jobs.result`, `vf.artifacts.inspect` | artifact_job | installed SDK: `verbiflow/crm.py:660-759` |
| `vf.crm.fetch_objects` | Use to create a Bridge-backed CRM object artifact from provider filters and selected properties. | <strong>Required</strong>: `object_type` (str; required): CRM object type. Current Bridge-backed CRM support is `contacts`, `companies`, and `deals`.<br><strong>Optional</strong>: `artifact_label` (str \| None; optional; default `None`): Optional label for the resulting CRM object artifact.; `filters` (list[JsonObject] \| JsonObject \| None; optional; default `None`): CRM filter specification. Use property/operator/value clauses supported by the CRM provider; start with `crm.list_properties` when unsure.; `limit` (int \| None; optional; default `None`): Maximum CRM object rows to fetch. Omit to use the backend default of 5000; the current backend cap is 50000.; `list_id` (str \| None; optional; default `None`): HubSpot list ID from crm.list_lists. Mutually exclusive with filters; the list object type must match object_type.; `properties` (list[str] \| None; optional; default `None`): CRM property names to include in the artifact rows. For HubSpot, discover names with `crm.list_properties`; the backend sends these as `include_properties` and caps the list at 50 properties.; `provider` (str \| None; optional; default `None`): CRM provider. Current Bridge-backed CRM support is HubSpot (`hubspot`). | `PendingJob`: `job_id` immediately; wrap with `vf.jobs.result(pending).wait(...)`, then use `.artifact()`, `.rows()`, `.to_report()`, or `.stage_to_sequence(...)` when `artifact_id` is present.<br><strong>Common next calls</strong>: `vf.jobs.result`, `vf.artifacts.inspect` | artifact_job | installed SDK: `verbiflow/crm.py:537-617` |
| `vf.crm.list_association_types` | Use before association fetches when the CRM provider requires a specific association type. | <strong>Required</strong>: `from_object_type` (str; required): CRM source object type for association fetches. Current Bridge-backed CRM support is `contacts`, `companies`, and `deals`.; `to_object_type` (str; required): CRM destination object type for association fetches. Current Bridge-backed CRM support is `contacts`, `companies`, and `deals`.<br><strong>Optional</strong>: `provider` (str \| None; optional; default `None`): CRM provider. Current Bridge-backed CRM support is HubSpot (`hubspot`). | `dict` with CRM association definitions for the selected object pair. | read | installed SDK: `verbiflow/crm.py:505-535` |
| `vf.crm.list_deal_stages` | Use to flatten deal pipeline stages before filtering, suppression, or stage-label reporting. | <strong>Required</strong>: none<br><strong>Optional</strong>: `include_archived` (bool; optional; default `False`): Include archived parameter for `crm.list_deal_stages`.; `provider` (str \| None; optional; default `None`): CRM provider. Current Bridge-backed CRM support is HubSpot (`hubspot`). | `crm_deal_stages` payload returned by the SDK operation. Inspect the documented function reference and `raw`/dictionary fields when debugging provider-specific data. | read | installed SDK: `verbiflow/crm.py:438-503` |
| `vf.crm.list_lists` | Use when reading CRM metadata or starting CRM object/association fetch jobs through Bridge. | <strong>Required</strong>: none<br><strong>Optional</strong>: `cursor` (str \| None; optional; default `None`): Opaque cursor from next_cursor.; `limit` (int; optional; default `100`): Maximum number of records, preview rows, or results to request.; `object_type` (str; optional; default `'contacts'`): CRM object type. Current Bridge-backed CRM support is `contacts`, `companies`, and `deals`.; `provider` (hubspot; optional; default `'hubspot'`): CRM provider. Current Bridge-backed CRM support is HubSpot (`hubspot`).; `query` (str \| None; optional; default `None`): Optional list-name search text. | `crm_lists` payload returned by the SDK operation. Inspect the documented function reference and `raw`/dictionary fields when debugging provider-specific data.<br><strong>Common next calls</strong>: `vf.crm.fetch_objects` | read | installed SDK: `verbiflow/crm.py:67-118` |
| `vf.crm.list_pipelines` | Use before deal-based filtering or reporting when pipeline/stage ids and labels matter. | <strong>Required</strong>: none<br><strong>Optional</strong>: `include_archived` (bool; optional; default `False`): Include archived parameter for `crm.list_pipelines`.; `object_type` (str; optional; default `'deals'`): CRM object type. Current Bridge-backed CRM support is `contacts`, `companies`, and `deals`.; `provider` (str \| None; optional; default `None`): CRM provider. Current Bridge-backed CRM support is HubSpot (`hubspot`). | `crm_pipelines` payload returned by the SDK operation. Inspect the documented function reference and `raw`/dictionary fields when debugging provider-specific data. | read | installed SDK: `verbiflow/crm.py:410-436` |
| `vf.crm.list_properties` | Use before building CRM filters or selecting fetch properties. | <strong>Required</strong>: `object_type` (str; required): CRM object type. Current Bridge-backed CRM support is `contacts`, `companies`, and `deals`.<br><strong>Optional</strong>: `provider` (str \| None; optional; default `None`): CRM provider. Current Bridge-backed CRM support is HubSpot (`hubspot`). | `dict` with CRM property metadata for the selected object type/provider. | read | installed SDK: `verbiflow/crm.py:42-65` |
| `vf.crm.status` | Use when reading CRM metadata or starting CRM object/association fetch jobs through Bridge. | <strong>Required</strong>: none<br><strong>Optional</strong>: `provider` (str; optional; default `'hubspot'`): CRM provider. Current Bridge-backed CRM support is HubSpot (`hubspot`). | `crm_status` payload returned by the SDK operation. Inspect the documented function reference and `raw`/dictionary fields when debugging provider-specific data.<br><strong>Common next calls</strong>: `vf.crm.list_properties` | read | installed SDK: `verbiflow/crm.py:25-40` |
| `vf.crm.upsert_to_list` | Use when reading CRM metadata or starting CRM object/association fetch jobs through Bridge. | <strong>Required</strong>: `list_id` (str; required): Writable MANUAL or SNAPSHOT HubSpot list identifier.; `object_type` (contacts \| companies; required): CRM object type. Current Bridge-backed CRM support is `contacts`, `companies`, and `deals`.; `records` (Sequence[JsonObject]; required): Use legacy {id, properties} rows with id_property, or resolution-aware {source_key, identifiers, properties} rows. Each identifier has kind, property, and value.<br><strong>Optional</strong>: `artifact_label` (str \| None; optional; default `None`): Human-readable label for the artifact created by the backend job.; `id_property` (str \| None; optional; default `None`): Legacy single unique property for every record. Omit when records use source_key plus identifiers for resolution-aware upsert.; `idempotency_key` (str \| None; optional; default `None`): Idempotency key parameter for `crm.upsert_to_list`.; `provider` (str; optional; default `'hubspot'`): CRM provider. Current Bridge-backed CRM support is HubSpot (`hubspot`). | `PendingJob`: `job_id` immediately; wrap with `vf.jobs.result(pending).wait(...)`, then use `.artifact()`, `.rows()`, `.to_report()`, or `.stage_to_sequence(...)` when `artifact_id` is present.<br><strong>Common next calls</strong>: `vf.jobs.result`, `vf.artifacts.inspect` | provider_job | installed SDK: `verbiflow/crm.py:204-289` |
<!-- END GENERATED: core-sdk-surfaces -->

## Data Contracts

| Contract | Meaning |
| --- | --- |
| `provider` | Current SDK/backend CRM provider is `hubspot`. Do not assume Salesforce, Attio, or other providers here unless the active function index/backend contract exposes them. |
| `object_type` | Current Bridge object types are `contacts`, `companies`, and `deals`. |
| `properties` / backend `include_properties` | Provider property names to include in object rows. Discover with `list_properties`; backend caps the selected list at 50. |
| `filters` | CRM provider filter clauses. SDK accepts one object or a list and sends a normalized list. For HubSpot, use provider property names and supported operators. |
| `list_id` | Stable HubSpot list identifier returned by `list_lists`. It is mutually exclusive with `filters`; list membership is resolved when the job runs, so dynamic lists stay current. |
| `next_cursor` | Opaque cursor returned by `list_lists`. Pass it back as `cursor` to page more list definitions; do not parse or increment it. |
| `limit` / backend `max_rows` | Maximum CRM object rows. Omitted SDK `limit` lets backend default to 5000; backend cap is 50000. |
| `from_object_ids` / backend `object_ids` | CRM object IDs from CRM rows. These are not SDK job IDs, artifact IDs, emails, or domains. Backend accepts up to 1000 source IDs per association job. |
| `match_values` / property `render` | `enrich_contacts` accepts at most 500 match values per call; chunk larger sets. Property items are names (render `raw`) or `{"name": ..., "render": "owner_name"}`; `owner_name` resolves owner ids and returns the value as `<property>_name` (for example `hubspot_owner_id` -> `hubspot_owner_name`). |
| `PendingJob` | Object/association fetches return `PendingJob`. Preserve `job_id` immediately and any `artifact_id`; use `vf.jobs.result(...)` before waiting, paging, reporting, or staging. |
| Property uniqueness | `list_properties` returns `hasUniqueValue` plus read-only metadata from HubSpot. `email` and `domain` are canonical unique identifiers. Any other create identifier must report `hasUniqueValue=true`. |
| Legacy upsert records | `upsert_to_list` accepts 1-1000 `{id, properties}` records with one request-level `id_property`. The property must be `email`/`domain` or provider-declared unique and writable. |
| Resolution-aware upsert records | Omit `id_property`. Each record is `{source_key, identifiers, properties}`. An identifier is `{kind, property, value}` where contact kinds are `email` or `linkedin_url`, and company kinds are `domain` or `linkedin_url`. Source keys and normalized identifiers must not be shared by input rows. |
| Identity receipt | `identity_status` is `single_property`, `matched`, `created`, `ambiguous`, `conflict`, `unresolvable`, or `unknown`. Preserve `provider_record_id`, `matched_identifiers`, `create_id_property`, `upsert_status`, `membership_status`, and `outcome`. |
| Association writes | `ensure_associations` accepts 1-1000 unique `{from_id, to_id}` pairs of resolved CRM record IDs. It reads the exact type/category edge first and creates only missing pairs. The default unlabeled provider-defined type is resolved and validated at execution; pass a positive type ID returned by `list_association_types` for an explicit label. |
| Association rows | Preserve source/destination object types, source CRM object id, destination CRM object id, association category/type, and provider metadata when present. |
| Suppression outputs | Keep explicit included/excluded audit rows for active customers, deal-associated companies, lifecycle-stage suppression, owner/routing gaps, or any other CRM-derived decision. |

## Examples

### Create, Populate, And Inspect A Manual List

```python
import verbiflow as vf

created = vf.crm.create_list("July product-qualified leads", "contacts")
job = vf.crm.upsert_to_list(
    object_type="contacts",
    id_property="email",
    list_id=created["list_id"],
    records=[
        {"id": "ada@example.com", "properties": {"firstname": "Ada"}},
        {"id": "grace@example.com", "properties": {"firstname": "Grace"}},
    ],
    idempotency_key="july-pql-import-v1",
)
receipt = vf.jobs.result(job).wait(timeout=300)
print(receipt.job_id, receipt.artifact_id, receipt.rows(limit=1000))
```

Every receipt row has independent `upsert_status`, `membership_status`, and
`outcome`. Retry or reconcile `unknown` rows using the same provider identity;
do not relabel an ambiguous provider result as failed.

### Resolve Email And LinkedIn Before Upsert

```python
import verbiflow as vf

properties = vf.crm.list_properties(object_type="contacts", provider="hubspot")
by_name = {item["name"]: item for item in properties["properties"]}
if by_name["vf_contact_linkedin_key"]["hasUniqueValue"] is not True:
    raise RuntimeError("vf_contact_linkedin_key must be a unique HubSpot property")

job = vf.crm.upsert_to_list(
    object_type="contacts",
    list_id="12345",
    records=[
        {
            "source_key": "stable-workflow-contact-key",
            "identifiers": [
                {"kind": "email", "property": "email", "value": "ada@example.com"},
                {
                    "kind": "linkedin_url",
                    "property": "hs_linkedin_url",
                    "value": "https://www.linkedin.com/in/ada",
                },
                {
                    "kind": "linkedin_url",
                    "property": "vf_contact_linkedin_key",
                    "value": "https://www.linkedin.com/in/ada",
                },
            ],
            "properties": {"firstname": "Ada", "jobtitle": "Founder"},
        }
    ],
    idempotency_key="contact-activation-v1",
)
receipt = vf.jobs.result(job).wait(timeout=300)
print(receipt.job_id, receipt.artifact_id, receipt.rows(limit=1000))
```

The standard LinkedIn property supports reconciliation with existing HubSpot
records. The dedicated `vf_contact_linkedin_key` supports race-safe creation
when email is absent. If email and LinkedIn resolve to different HubSpot IDs,
the row returns `conflict` and no provider write occurs.

### Ensure Contact-Company Associations

```python
association_job = vf.crm.ensure_associations(
    from_object_type="contacts",
    to_object_type="companies",
    pairs=[
        {"from_id": "resolved-contact-id", "to_id": "resolved-company-id"},
    ],
    idempotency_key="llamaindex-account-links-2026-07-21-v1",
)
association_receipt = vf.jobs.result(association_job).wait(timeout=300)
print(association_receipt.rows(limit=1000))
```

Object upsert, contact-list membership, and contact-company association are
three independently receipted effects. A later phone backfill should upsert the
same contact identity; it must not create another contact, list membership, or
association merely because the enriched field set changed.

### Delete A List (Destructive)

```python
# Obtain explicit human confirmation immediately before this call.
deleted = vf.crm.delete_list("12345")
print(deleted["list_id"], deleted["deleted"])
```

Workflow Plays must put `delete_list` behind an explicit confirmation step.

### Fetch CRM Contacts

```python
import verbiflow as vf

vf.auth.ensure_logged_in()
status = vf.crm.status(provider="hubspot")
print(status["provider"], status["connected"], status.get("blocked_reason"))

properties = vf.crm.list_properties(object_type="contacts", provider="hubspot")
print("contact property count", len(properties.get("properties", [])))

job = vf.crm.fetch_objects(
    object_type="contacts",
    provider="hubspot",
    properties=["email", "firstname", "lastname", "associatedcompanyid", "createdate"],
    filters={"field": "email", "operator": "HAS_PROPERTY"},
    limit=500,
    artifact_label="hubspot contacts with email",
)
result = vf.jobs.result(job).wait(timeout=300)
rows = result.rows(limit=25)
print(result.job_id, result.artifact_id, len(rows), rows[:2])
```

### Fetch The Current Members Of A HubSpot List

```python
import verbiflow as vf

target_name = "Open opportunities"
page = vf.crm.list_lists(
    object_type="contacts",
    provider="hubspot",
    query=target_name,
    limit=100,
)
exact_matches = [
    item
    for item in page.get("lists", [])
    if item.get("name", "").strip().casefold() == target_name.casefold()
]
if len(exact_matches) != 1:
    raise RuntimeError(
        f"Expected one exact HubSpot contact list named {target_name!r}; "
        f"found {len(exact_matches)}"
    )

selected = exact_matches[0]
if selected.get("processing_status") != "COMPLETE":
    raise RuntimeError(f"HubSpot list is still processing: {selected['list_id']}")
print(selected["list_id"], selected["name"], selected.get("size"))

job = vf.crm.fetch_objects(
    object_type="contacts",
    provider="hubspot",
    list_id=selected["list_id"],
    properties=["email", "firstname", "lastname", "jobtitle"],
    limit=5000,
    artifact_label=f'HubSpot list: {selected["name"]}',
)
result = vf.jobs.result(job).wait(timeout=300)
print(result.job_id, result.artifact_id, result.rows(limit=10))
```

For a sequence-owned recurring source, pass the same canonical ID in the CRM
source config with `selection_mode="list"`, `list_id=selected["list_id"]`, and
the matching `data_type`. Sequence authoring and mutation gates remain owned by
`sequence_authoring.md`.

### Fetch Company Deal Associations

```python
import verbiflow as vf

company_ids = ["12345", "67890"]
jobs = vf.crm.fetch_associations_all(
    from_object_type="companies",
    to_object_type="deals",
    from_object_ids=company_ids,
    provider="hubspot",
    artifact_label="company deal associations",
    wait=True,
)
for job in jobs:
    result = vf.jobs.result(job)
    rows = result.rows(limit=25)
    print(result.job_id, result.artifact_id, len(rows))
```

## Verification

- Status shows the expected provider and `connected=True` before provider work.
- Property metadata includes every property used in filters, selected columns,
  suppression logic, routing logic, or identity resolution, including truthful
  `hasUniqueValue` and read-only flags.
- Object fetch results have `job_id`, `artifact_id`, expected row count, and
  non-empty CRM object IDs.
- List discovery returns the expected object type, stable `list_id`, current
  size, and `processing_status="COMPLETE"` before acquisition.
- Association fetch inputs are CRM object IDs sampled from fetched CRM rows.
- Association rows join back to the expected source CRM object IDs.
- Suppression/routing decisions have visible counts and sample rows for kept,
  excluded, and unmatched records.
- Downstream sequence staging or workflow reports preserve CRM provenance
  columns needed to audit the decision later.
- Upsert verification partitions every requested row by `identity_status`,
  `upsert_status`, and `membership_status`; successful contact-company pairs
  retain both resolved provider IDs before association.

## Troubleshooting

| Symptom | First Check | Fix |
| --- | --- | --- |
| CRM status is disconnected | `vf.crm.status(provider="hubspot")` and `auth.md` workspace focus | Focus the correct workspace or reconnect HubSpot before fetches. |
| Property filter returns no rows | `vf.crm.list_properties(...)` for exact provider property name and option values | Replace labels with real property names/options; sample a smaller query first. |
| HubSpot lists return 403 | The connection predates the required `crm.lists.read` scope | Reconnect HubSpot in Integrations, then retry `vf.crm.list_lists(...)`. |
| HubSpot reconnect reports a different portal | The workspace is already bound to another HubSpot portal | Reconnect the previously connected portal. The platform rejects silent portal switching to protect exclusion and sync semantics. |
| List fetch says object type differs | The selected list's `object_type` versus `fetch_objects(object_type=...)` | Use the object type returned by `list_lists`; do not reuse a contact list ID for companies or deals. |
| List is still processing | `processing_status` from `list_lists` | Wait for HubSpot to finish processing the list, then retry. |
| `fetch_associations` fails before Bridge | Empty `from_object_ids` or wrong ID source | Use CRM object IDs from fetched rows; do not pass artifact/job IDs, domains, or emails. |
| Association output is empty | Object type direction, association type, and sample CRM IDs | Try the reverse object direction only after verifying provider graph semantics; list association types first. |
| Association ensure returns `unknown` | Provider attempt receipt and per-pair receipt | Reconcile the same resolved pair and idempotency key. Do not create replacement CRM objects or silently mark it failed. |
| LinkedIn-only row is `unresolvable` | `hasUniqueValue` for the supplied LinkedIn identity property | Keep the standard LinkedIn field for matching, and provision a normalized unique `vf_*` LinkedIn key before allowing creation. Do not create by name or a non-unique display field. |
| Identity row is `ambiguous` | One supplied non-unique property matched several HubSpot records | Resolve the HubSpot duplicate manually. Do not select the oldest match or create another record. |
| Identity row is `conflict` | Supplied identifiers resolved to different HubSpot record IDs | Hold the row for CRM reconciliation. Do not merge or overwrite customer CRM records automatically. |
| Deal suppression misses companies | Company-deal association fetch, deal pipeline/stage IDs, archived/closed semantics | Fetch deal stages and associations explicitly, then join by CRM company ID. |
| Output looks stale after workflow code changed | Workflow checkpoint/source cache semantics | Use `workflow_sources.md` and `workflow_operations.md`; CRM docs only define provider request shape. |
| Need all rows from a large artifact | Artifact paging/export, not preview rows | Use `vf.jobs.result(job).iter_rows(...)` or `vf.artifacts.export_csv(...)` from `artifacts_jobs.md`. |

## Related References

| If you are doing this | Read next |
| --- | --- |
| Workspace auth, profile focus, or integration readiness debugging | `auth.md` |
| CRM rows are one source inside a recurring workflow | `workflow_sources.md` |
| Designing a multi-step play that combines CRM, discovery, enrichment, and reports | `workflow_composition.md` |
| Waiting for CRM jobs, paging rows, exporting CSVs, joining artifacts | `artifacts_jobs.md` |
| Local joins, suppression filters, routing columns, CSV compatibility | `row_operations.md` |
| People/company discovery after CRM account selection | `audience_discovery.md` |
| Email, phone, profile, company, or identity enrichment after CRM fetches | `enrichment.md` |
| Sequence source staging, publish, send, or repair after CRM suppression | `sequence_authoring.md`, `sequences.md`, `safety.md` |
| Final Workflow Play report output for CRM-derived rows | `workflow_reports.md` |
