HubSpot CRM ExecModule
Overview
HubSpotCrmModule connects ValkyrAI workflows to HubSpot CRM v3 for contacts, companies, and deals. It creates and updates one record at a time, retrieves one record, or returns one bounded list or search page.
Version 2.0 replaces misleading “feature-complete” claims, empty input/output metadata, unreachable actions, inert tests, and unbounded provider access. The module now has an explicit 15-operation contract, a READY IntegrationAccount boundary, strict record/property/search validation, bounded request and response bodies, 10-second connection and 30-second read timeouts, verified response identities, normalized confidential outputs, and safe errors that never retain provider bodies or credentials.
Read-only get, list, and search calls retry HTTP 429 and transient 5xx responses at most twice after the first attempt. Create and update operations never retry automatically because HubSpot may have committed a write before a timeout or ambiguous provider response.
Usage
- Create a HubSpot private app with only the contact, company, and deal read/write scopes required by the selected operations.
- Store its access token only in the encrypted
apiKeyfield of a ValkyrAIIntegrationAccountand set the account toREADY. - Bind that record through
ExecModuleConfig.authConfig.integrationAccount. - Select an operation and supply only its required fields.
- For list or search operations, consume the returned
aftercursor in a later workflow execution whenhasMoreistrue. - Treat all returned CRM properties as confidential or restricted workflow data.
Inputs
| Name | Type | Required | Description | Local constraints |
|---|---|---|---|---|
operation | string | Yes | HubSpot operation. | One of the 15 operations listed below. |
record_id | string | Conditional | Record ID for get_* and update_*. | 1–256 safe identifier characters; path separators and controls are rejected. |
properties | object | Conditional | Property map for create_* and update_*. | Non-empty; at most 200 safe property names; scalar values only; serialized request at most 512 KB. |
filter_groups | array | Conditional | HubSpot search filters. | A search requires query or filter_groups; at most five groups, six filters per group, and 18 filters total. |
query | string | Conditional | HubSpot text-search query. | At most 3,000 Unicode code points and no control characters. |
sorts | array of strings | No | Search sort expressions. | At most five bounded strings. |
property_names | array of strings | No | Properties to include in provider responses. | At most 100 unique safe HubSpot property names. |
after | string | No | Opaque HubSpot pagination cursor. | At most 1,024 characters and no controls; never interpret or modify it. |
limit | integer | No | Provider page size. | Defaults to 100; list calls allow 1–100 and search calls allow 1–200. |
Create and update property values must be scalar JSON values. Nested objects and arrays are rejected so the connector cannot silently create an unsupported HubSpot property shape. create_company additionally requires properties.name or properties.domain; create_deal requires properties.dealname and properties.dealstage.
Outputs
| Name | Type | When present | Description |
|---|---|---|---|
hubspot.status | string | Always | SUCCESS after verified normalization or ERROR on failure. |
hubspot.operation | string | Success | Normalized operation name. |
hubspot.objectType | string | Success | contacts, companies, or deals. |
hubspot.objectId | string | Single-record success | Verified provider record ID. |
hubspot.data | object | Single-record success | The bounded HubSpot record response. |
hubspot.items | array | List or search success | One bounded provider page of records. |
hubspot.count | integer | List or search success | Number of items in this page. |
hubspot.hasMore | boolean | List or search success | Whether HubSpot supplied a next cursor. |
hubspot.after | string | When another page exists | Opaque next-page cursor. |
The runtime ExecModule is also marked GOOD or ERROR. CRM payloads are written to WorkflowState without being copied into progress or error logs. EventLogs contain only the operation, attempt count, and sanitized failure class.
IntegrationAccount Requirements
| Setting | Requirement |
|---|---|
| Provider | HubSpot CRM API v3 |
| Authentication | HubSpot private-app access token |
| Status | Exactly READY |
accountName | Human-readable HubSpot portal/private-app identity |
apiKey | Encrypted private-app token, 20–2,048 characters, with no whitespace or controls |
| Relationship | Bind through ExecModuleConfig.authConfig.integrationAccount |
Grant only the provider scopes needed by the chosen object types and actions, such as contact, company, or deal read/write scopes. Never place the token in payload fields, URLs, logs, examples, documentation, or workflow state.
Configuration
Illustrative normalized configuration:
{
"version": "2.0.0",
"authConfig": {
"authStrategy": 1,
"integrationAccount": "integration-account:hubspot-sales-private-app"
},
"payloadConfig": {
"parameters": "{\"operation\":\"search_contacts\",\"limit\":50,\"filter_groups\":[{\"filters\":[{\"propertyName\":\"lifecyclestage\",\"operator\":\"EQ\",\"value\":\"lead\"}]}]}"
}
}
The account value is a symbolic secure relationship. Persisted workflows use the generated IntegrationAccount reference, never a plaintext token.
Operations
| Object | Create | Get | List | Search | Update |
|---|---|---|---|---|---|
| Contacts | create_contact | get_contact | list_contacts | search_contacts | update_contact |
| Companies | create_company | get_company | list_companies | search_companies | update_company |
| Deals | create_deal | get_deal | list_deals | search_deals | update_deal |
Provider requests use the corresponding /crm/v3/objects/{contacts|companies|deals} endpoint. Create uses POST; get and list use GET; search uses POST .../search; update uses PATCH .../{record_id}.
List creation, dynamic/static segment management, list membership mutation, record deletion, batch mutation, object association, owners, pipelines, custom-object discovery, and property-schema administration are not implemented. Version 2.0 deliberately removes those previously advertised but unreachable behaviors from this module’s contract.
Errors and Failure Modes
| Failure | Cause | Retry guidance |
|---|---|---|
| Validation failure | Missing/unsupported operation, unsafe ID, invalid property map, missing company/deal fields, malformed filters, or a local size/page limit. | Correct the configuration; no request was sent. |
| IntegrationAccount failure | No bound account, non-READY status, or malformed private-app token. | Repair the secure account; no request was sent. |
| HTTP 400/404 | Provider validation failure or missing record. | Correct the record, property names, filter, pipeline, or stage before retrying. |
| HTTP 401/403 | Invalid token or missing private-app scope. | Reauthorize the IntegrationAccount with the minimum required scopes. |
| HTTP 429 | HubSpot rate limit. | Read calls use bounded retry; after exhaustion, honor the provider retry window. Writes are never retried automatically. |
| HTTP 5xx or network failure | Provider or transport failure. | Read calls use bounded retry. Reconcile create/update state in HubSpot before rerunning an ambiguous write. |
| Invalid success response | Empty/oversized/invalid JSON, missing result array, unsafe record ID, or get/update ID mismatch. | Treat as failure and investigate provider/API compatibility; success is not recorded. |
Provider response bodies can contain CRM properties and credential-like diagnostic text. Failures therefore expose only a sanitized status, attempt count, and retry boundary. The original provider exception is not retained as the emitted error cause.
Example
Search for contacts in the lead lifecycle stage:
{
"operation": "search_contacts",
"filter_groups": [
{
"filters": [
{
"propertyName": "lifecyclestage",
"operator": "EQ",
"value": "lead"
}
]
}
],
"property_names": ["email", "firstname", "lastname", "lifecyclestage"],
"limit": 50
}
Expected normalized result when one page contains one record and no continuation:
{
"hubspot.status": "SUCCESS",
"hubspot.operation": "search_contacts",
"hubspot.objectType": "contacts",
"hubspot.items": [
{
"id": "12345",
"properties": {
"email": "lead@example.com",
"lifecyclestage": "lead"
}
}
],
"hubspot.count": 1,
"hubspot.hasMore": false
}
Notes
- Pagination: each execution returns one page only. Pass the exact
hubspot.aftervalue into a later execution; the module does not paginate silently. - Limits: list pages cap at 100 and search pages at 200. Search payloads cap at 3,000 UTF-8 bytes, object requests at 512 KB, and provider responses at 5 MB.
- Rate limits: read-only transient HTTP failures are retried up to three total attempts with a bounded delay. Provider
Retry-Afterseconds are honored up to five seconds; HTTP-date values fall back to a bounded local delay. - Idempotency: reads are safe to retry. Create operations are not idempotent, and update success can be ambiguous after a transport failure. The module never treats WorkflowState as provider deduplication.
- API constraints: HubSpot search indexing can lag newly created or updated records. Filters and property names must use HubSpot internal property identifiers.
- Destructive behavior: record deletion and list membership mutation are not supported. Create/update still change external CRM state and can trigger HubSpot automation.
- Confidentiality: returned contact, company, and deal properties are classified
restricted; downstream steps must preserve tenant RBAC/ACL and data-minimization rules. - Observability: progress logs contain operation names and attempt counts, not tokens, property values, CRM responses, or provider error bodies.
- Unverified boundary: deterministic tests cover request construction, validation, exact account readiness, read retry, pagination normalization, response identity, metadata serialization, no write retry, and token redaction. Live HubSpot execution requires separately authorized provider credentials and is not exercised by repository tests.
See HubSpot’s official CRM contacts guide, companies guide, deals guide, and CRM search limits for provider behavior.