HubSpot facts, with sources
These are the facts Spriggit uses when it plans a change. Most say where they come from: HubSpot’s documentation, a test on a HubSpot test portal, or both, with the date it was checked. A fact still waiting for a public source is labeled Source pending.
- DocumentedHubSpot’s documentation says so
- MeasuredTested in a HubSpot test portal; the page says which kind
- Documented and measured
- DisputedSources disagree; the page says how
- CorrectedAn earlier version was wrong; the page says what changed
- Source pendingNo public source yet
API
- From
2026-09, conditional required properties and the "Create Record" form's required fields and associations are enforced on API writes, which fail with 400.Source pending - A DAILY 429 isn't retried: nothing more can be sent until the next local midnight in the account's time zone.Source pending
- Escaping the portal's daily API limit depends on the app's
distribution: "marketplace", not on using OAuth.Source pending - Hitting the daily API limit shows as a 429 whose body has
policyName: "DAILY".Source pending - Date-based API versions put the version in the path where
/v3used to be, e.g./crm/objects/2026-09/contacts.Documented - All legacy semantic-version paths
/v1to/v4, including associations v4 and automation v4, keep working until they become unsupported in September 2027.Documented - The CRM Limits API (
/crm/limits/2026-09/…, usage for custom objects, properties, pipelines, labels and records) exists only as a date-based version; there is no legacy path.Source pending - A response without the daily rate-limit headers only shows the call was made with OAuth; it doesn't show the call is exempt from the daily limit.Source pending
- Forms, automation flows, HubDB, business units, campaigns and marketing emails have no GA date-based version yet, so they are called on their legacy paths.Source pending
GET /account-info/{v}/api-usage/daily/private-appsanswers an OAuth token with one row,private-apps-api-calls-daily, but itscurrentUsagedoes not count a public app's calls.Measured- Private apps and service keys get 100 requests per 10 seconds and 250,000 a day on Free and Starter, 190 per 10 seconds and 625,000 a day on Professional, and 190 per 10 seconds and 1,000,000 a day on Enterprise.Documented
- CRM search endpoints are limited to 5 requests per second.Documented
- HubSpot ships a date-based API version twice a year, in March and September, and supports each for 18 months: 6 months as "current", then 12 months as "supported".Documented
Associations
- Take
categoryandtypeIdfrom the row the ordered pair's labels call returns, never assume a category: a custom object's unlabeled default (label: null) isUSER_DEFINED, a standard pair's isHUBSPOT_DEFINED.Measured - Association label rows carry
fromObjectTypeIdandtoObjectTypeIdasnull, so the object pair comes from the request path, never from the row.Measured - Up to 50 association labels can be defined per object pair, on Professional or higher.Source pending
GET /crm/associations/{v}/{from}/{to}/labelsreturns only that direction'stypeIds; the reverse order returns different ones, so read both orders of a pair.Measured- Association limits are written as
maxToObjectIdsper association type (Professional or higher) and read back asuserEnforcedMaxToObjectIds.Documented and measured - Limits tracking (
/crm/limits/{v}/associations/records/...) reports only the technical per-pair limit and ignores user-enforced limits; a pair read can return 500 while such a limit is set.Documented and measured - HubSpot-defined association type IDs are the same in every portal: contacts to companies 279 (unlabeled), 1 ("Primary") and 931; companies to contacts 280, 2 and 930; contacts to deals 4; tasks to contacts 204.Measured
- Association type ids between a custom object and a standard object (and the custom object's own type id) differ by portal; read them with
GET /crm/v4/associations/{fromObjectType}/{toObjectType}/labelsinstead of hard-coding them.Measured - HubSpot enforces a configured association limit: a batch create that would exceed it returns 207 with nothing created.Measured
- v4 association batch-create results carry numeric
fromObjectIdandtoObjectId, not thefrom: {id}/to: {id}shape the request uses.Measured
Auth
hs api <endpoint> --account=<id> --jsonauthenticates with the CLI's personal access key, which reaches portals that have nopat-token, including the legacy/content/api/v2/*endpoints.Measured- Legacy private apps and legacy public apps become unsupported in September 2027; existing private-app tokens keep working until then.Documented
- A public OAuth app can't read object tags:
GET /crm/object-tags/{v}/0-3returns 403 "The scope needed for this API call isn't available for public use".Measured - The CLI's personal access key isn't usable as a bearer token (401
EXPIRED_AUTHENTICATION); onlypat-private-app tokens are.Measured - Creating legacy private apps is disabled for new accounts from September 28, 2026 and for existing accounts from October 26, 2026; service keys replace them.Documented
- A private-app (
pat-) token is rejected by the OAuth introspection endpointGET /oauth/v1/access-tokens/{token}; read the portal fromGET /account-info/v3/detailsinstead.Measured - Creating or changing properties needs the per-object schema write scope, e.g.
crm.schemas.contacts.write,crm.schemas.companies.write,crm.schemas.deals.write.Source pending - Tickets use the granular
crm.objects.tickets.read,crm.objects.tickets.write,crm.schemas.tickets.readandcrm.schemas.tickets.writescopes. They replaced the singleticketsscope in September 2026: an app that still requeststicketsfails to deploy, but installs made before keep it, and their tokens still reporttickets.Documented - A service key can only be granted scopes that the user creating it has.Source pending
- Service keys can't receive webhooks or power app cards; those need a developer project (
webhookscomponent, app cards) built with the HubSpot CLI.Documented - Service keys are HubSpot's replacement for legacy private apps.Documented
Campaigns
- Attaching an asset to a campaign is
PUT /marketing/v3/campaigns/{campaignGuid}/assets/{assetType}/{assetId}, which answers 204. Measured forFORMandMARKETING_EMAIL.Measured WORKFLOWandCASEare not campaign asset types (both return 400); the valid names areAUTOMATION_PLATFORM_FLOWandCASE_STUDY. The 400 for an invalid type lists the valid set.Measured- The Campaigns API needs Marketing Hub Professional or higher.Source pending
- A campaign read returns 7 default properties plus
assetsunlesspropertiesnames the ones wanted. Withproperties, the default system properties are dropped and unset properties are absent, not null.Measured
Cards
- App cards (UI extensions) have no REST API; they are built as a developer project with the HubSpot CLI on platform 2026.09.Source pending
- Classic CRM cards (the
/crm/v3/extensions/cardsAPI) were deprecated on June 16, 2025.Documented - Classic CRM cards are sunset on November 2, 2026 and must be rebuilt as app cards; the migration UI and migrate API run until December 1, 2026. This replaces the earlier October 31 date.Corrected
- Classic CRM cards stop working on October 31, 2026.Documented
- App cards can't use
windoworfetch; outbound calls go throughhubspot.fetchto URLs listed inpermittedUrls.fetch.Source pending
CMS
Connections
- An invalid or mistyped
--accounton the HubSpot CLI silently uploads to the default account and still reports SUCCESS; check the account name in the success line.Measured - Composio's managed HubSpot OAuth app appears not to request the
forms,crm.lists.writeorsettings.users*scopes; a Composio auth config using your own HubSpot OAuth app is needed for those.Source pending - Composio's proxy-execute endpoint must be called on API v3.1; the proxy is disabled on v3.Source pending
- Composio's dedicated HubSpot workflow tools target the v3 workflows API, so Automation v4 flows go through
proxy_executeinstead.Source pending
Consent
- "Manage communication subscriptions" is workflow action
0-43347357(v8). Its fields are plain strings:targetContact,channel(EMAIL),optState(OPT_OUTorOPT_IN),subscriptionId,legalBasisandlegalBasisExplanation.Measured GET /communication-preferences/v4/statuses/{email}?channel=EMAILreturnsOBJECT_NOT_FOUNDfor a contact that has no subscription record yet (treat it as never set); after a change it returns one row per type withSUBSCRIBED,UNSUBSCRIBEDorNOT_SPECIFIED.Measured- A per-type status change is
POST /communication-preferences/v4/statuses/{email}with{channel: "EMAIL", subscriptionId, statusState: "UNSUBSCRIBED" | "SUBSCRIBED", legalBasis, legalBasisExplanation}.Measured - Each subscription type has a contact property
hs_email_optout_<id>(the type'sid): a HubSpot-defined, read-only single checkbox labelled "Opted out of email: <name>". The globalhs_email_optoutsits beside them, and workflow filters can use both.Measured - Subscription types can't be created or deleted through the API: a
POST /communication-preferences/v4/definitionsanswers 405. They are created one at a time in Settings > Marketing > Email > Subscription Types; reading them works.Documented and measured - In the "Manage communication subscriptions" workflow action the subscription type is a fixed choice, not a token, so a workflow that must act on different types per record needs one branch and one action per type.Measured
GET /communication-preferences/v4/definitionslists the portal's subscription types, including to a public OAuth app:id(a numeric string),name,purpose,communicationMethod,isActive,isDefault,isInternalandbusinessUnitId.MeasuredPOST /communication-preferences/v4/statuses/{email}/unsubscribe-all?channel=EMAILsets every subscription type for the contact, including the default ones, toUNSUBSCRIBED.Measured
Engine
Errors
- Enabling a workflow can fail with a generic 400 "Invalid request to flow update" whose message names no field, while the same update with the workflow off succeeds. In the measured case the cause was a dropdown label sent in place of its internal value.Measured
- When a list filter branch is missing a required enum field,
POST /crm/v3/listsanswers "Some required fields were not set: [operator]"; sending a deliberately invalid value returns an error that names the accepted value(s).Measured
Events
- Custom behavioral events need an Enterprise subscription.Source pending
- Dated (
2026-09) custom event definition reads carrylabels.singularandlabels.plural, and nolabel.Source pending - A flow or list filter's
eventTypeIdequals the custom event definition'sobjectTypeId(6-<definition id>); HubSpot's own events appear as4-<n>.Measured - A custom event's fully qualified name, used to send and read it, is
pe<hubId>_<name>, so it differs per portal; its definition also gets an event type ID6-<n>. Key definitions onnameorobjectTypeId.Documented and measured - Custom event property enumeration options are stored with their
valueexactly as sent.Source pending - A custom event defined on a custom object takes the object's short name as
primaryObject. The object type ID (2-…), the portal-qualifiedp<hub>_<name>and the upper-cased name are rejected.Measured - Reading a custom-object record's event occurrences with
GET /events/v3/eventsneedsobjectTypeset to the object's type id (2-<n>); the object's short name fails.Measured - A custom event occurrence on a custom-object record is sent with
POST /events/v3/sendand{ eventName: "pe<hubId>_<name>", objectId, occurredAt, properties }.Measured
Filters
Forms
- A Forms v3 create (
POST /marketing/v3/forms) is rejected unless the body includescreatedAt.Measured - In the forms v3 API a hidden dropdown field's preset value goes in
defaultValues: ["<value>"]; adefaultValuestring is silently dropped.Measured - A form's
notifyRecipientsis a list of HubSpot user IDs as strings, not email addresses.Source pending - A form can be submitted by API without auth at
POST https://api.hsforms.com/submissions/v3/integration/submit/{portalId}/{formGuid}, and it triggers workflows like a browser submission;api.hubapi.comhas no submit route (404).Measured - A form's
configuration.lifecycleStagesneeds an entry for contacts (objectTypeId: "0-1") and one for companies ("0-2"), each a stage's internal value (subscriber), even when the form has contact fields only. A contacts entry alone answers 400Lifecycle stage provided were not valid(VALIDATION_ERROR). An empty list is accepted.Documented and measured
Lead scoring
- A score criterion counting events on associated records uses
{type: ASSOCIATION_UE, associatedObjectTypeId, associationSpec, inverseAssociationSpec, rollupType: SUM, associationFilters: {propertyFilters: [...]}, recordCalculation: {type: MULTI_UE, eventCalculators: [...]}}.Measured PUT …/scores/{id}/draftreplaces the whole draft and can only override outputs that already exist. A published score has no draft:GET …/scores/{id}/draftreturns 404DRAFT_NOT_FOUND.Measured- An event criterion built by API as
MULTIofCONDITIONALs scores but the score editor shows "We couldn't load your criterion"; the editor-compatible form is oneeventCalculatorsentry per criterion withrecordCalculation: {type: MATCH, property, matchRules: [...]}.Measured - A lead score property rule must be a top-level
MULTIofCONDITIONALcalculators whosewhereConditionis aCOMPOSITEAND ofSINGLEconditions. A bareCONDITIONALorMATCHat the top returnsINVALID_TOP_LEVEL_CALCULATOR_TYPE.Measured - HubSpot appends a numeric suffix to a lead score's output property names (for example
<name>_total_<number>), so the names must be read back after the score is created.Measured - The lead scoring API is a beta at version
2026-09-betaand needs a Professional subscription or higher.Source pending - The lead scoring API answers at
/marketing/lead-scoring/2027-03-beta/scores, and only after the portal opts into the beta: before that, every call returns 403LEAD_SCORING_PUBLIC_API_NOT_ENABLED.Disputed - Both lead scoring versions exist:
/marketing/lead-scoring/2026-09-beta/scoresand.../2027-03-beta/scoresare routed (an unknown version is 404), and both refuse a public app with 403 "isn't available for public use". Which one a portal with the beta opt-in answers on is unmeasured; Spriggit uses 2027-03-beta.Corrected - HubSpot recalculates a lead score about 0.5 to 8 minutes after the scored event, and up to about 8 minutes after the score is republished.Measured
- Creating a lead score with
POST /marketing/lead-scoring/{version}/scoresincludingoutputConfiguration: { outputProperty, syncEnabled: true }is the reliable way to get its score properties; outputs set in the score editor sometimes didn't save despite a success message.Measured - In score criteria,
STRING IS_EQUAL_TOon a dropdown event property is refused as "not supported by scoring"; useENUMERATION IS_ANY_OF(andMULTISTRING CONTAINSfor text), which work.Measured - A lead score group's limits must be symmetric (
minValue: -100, maxValue: 100) or left unset.Measured
Lists
- A list filter on associated records is a
filterBranchType: ASSOCIATIONbranch nested in the AND group, withobjectTypeId,associationTypeId,associationCategoryandoperator: IN_LIST, the only operator value HubSpot accepts there.Measured - Filters inside one
ASSOCIATIONlist branch are ANDed on the same associated record, so two conditions there mean one associated record meets both.Measured GET /crm/lists/{v}?listIds=...accepts 101 IDs and silently leaves out IDs that don't exist, so a list missing from a batch is "not found", not an error.Measured- A list read with
includeFilters=truecarriesfilterBranchOperatoron every filter branch, and an association branch carriesobjectTypeId,associationTypeId(a number),associationCategoryandoperator: IN_LIST.Measured - A newly created list does not show up in list reads right away: some lists were missing from a list inventory immediately after creation (indexing lag).Measured
- From
2026-09, list search requireslistIdsandprocessingTypesin the request body.Source pending
Objects
- Custom objects need an Enterprise subscription.Documented
- An account can have up to 10 custom object definitions.Documented
- Object library activation (appointments, courses, listings, services) has no write API; the API is read-only, and activation is done in Settings → Data Management → Data Model.Source pending
- From
2026-09, creating a schema requiresallowsSensitiveProperties,searchableProperties,secondaryDisplayPropertiesandshouldCreateSameObjectAssociation.Source pending - A custom object schema can only be deleted when it has zero records.Source pending
- Hard-deleting a custom object schema, so its name can be reused, is
DELETE …?archived=true; the old/purgeendpoint is retired.Documented - In a custom object schema,
idis the numeric part ("<n>") andobjectTypeIdis2-<n>, soid != objectTypeId; key schemas onobjectTypeId.Measured - A custom object's internal
nameis permanent once the schema is created.Source pending
Pipelines
- Won and lost deal stages are marked with
isClosedin the stage metadata.Source pending - Every deal stage needs a
probabilityin its metadata, a number from 0 to 1 sent as a string.Source pending - From
2026-09, pipeline and stage deletes validate references by default.Documented - Pipelines allowed per object by tier: Free 1, Starter about 2, Professional 15, Enterprise 100.Source pending
- Pipeline rules and approvals have an API from version
2026-09, and need a Professional subscription or higher.Source pending - Deleting a pipeline stage that live records sit in returns 400
Stage IDs: [<stageId>] are being referenced by object IDs: [<recordId>]; archived records don't block a pipeline delete.Documented and measured - Ticket stages carry
ticketStateOPENorCLOSEDin their metadata.Source pending
Properties
- Each object can have up to 1,000 custom properties.Source pending
- A property's
name,typeandhasUniqueValuecan't be changed after it is created.Source pending - HubSpot's default
industryproperty is an enumeration, not a free-text field.Measured - HubSpot keeps sparse option
displayOrdervalues as sent on create (5, 1, 9), and a PATCH oflabel,descriptionoroptionsdoesn't renumber the remaining options.Measured - Removing an option that records hold succeeds, and the records keep the value (search still finds it, no history entry); re-adding the option makes it valid again with no record write.Measured
- A property's
optionsarray is not always returned indisplayOrderorder (a HubSpot ticket property returns 0, 1, 3, 2); sort bydisplayOrder, thenvalue, before comparing.Measured - PATCH on a property replaces its whole
optionslist; options left out of the body are removed.Source pending - An archived property can't be un-archived with
PATCH archived: false(400 "Cannot find property"); aPOSTwith its name restores it with its originalcreatedAtand record values, taking the new request body.Measured - Roll-up properties cannot be created through the properties API:
POST /crm/v3/properties/{objectType}withfieldType: calculation_rollupreturns 400 "Enum type must be one of: [...]" (the list includescalculation_equationbut no roll-up). Create roll-ups in Settings > Properties.Measured GET /crm/properties/{v}/{object}?dataSensitivity=sensitivewithout a sensitive-data scope returns 200{"results": []}, not 403, so an empty result proves nothing.Measured- Stage timing properties are
hs_v2_date_entered_<stage>,hs_v2_date_exited_<stage>,hs_v2_latest_time_in_<stage>andhs_v2_cumulative_time_in_<stage>; there are nohs_date_entered_*names.Measured hs_analytics_sourceand other HubSpot system properties are read-only and can't be set on record writes.Source pending- Property validation rules are enforced on API record writes from version
2026-09: a value that fails a rule returns 400Property values were not valid(for exampleFAILS_REGEX_MATCH).Documented and measured - The property validation rules API has no DELETE endpoint.Documented and measured
Records
- An archived contact keeps its email reserved: re-creating a contact with that email returned 409
Contact already exists.Disputed - Creating a contact with an archived contact's email makes a new record with a new ID and none of the old values.Disputed
- A batch create silently collapses identical inputs inside one request: 100 identical contacts returned 201 and created 1 to 3. Compare the created count with the input count.Measured
- Batch create results aren't guaranteed to come back in input order; map created IDs back by a unique property, never by position.Measured
- HubSpot enforces contact email uniqueness on create, so exact-email duplicate contacts can't be seeded through the API.Measured
- On contacts,
hs_lastmodifieddateis null and a filter on it matches nothing; uselastmodifieddate(equal toupdatedAt). Deals and companies havehs_lastmodifieddateonly.Measured - HubSpot refused a contact email on the reserved
.exampletop-level domain ("The email address format for Email is invalid") when a workflow created the contact;@example.comaddresses were accepted by the records API.Measured hs_lastmodifieddatecan't be backdated through the API.Measured- No API restores an archived CRM record: a PATCH by its ID returns 404 and the only restore is the in-app recycle bin (90 days). It still reads with
archived=true.Documented and measured - CRM search returns at most 10,000 results: a page with
after + limitpast 10,000 returns a generic 400 ("There was a problem with the request."), not a truncated page.Documented and measured - CRM search
totalis exact above 10,000, andlimit: 0is accepted and returns the count with no results.Measured
Reporting
Settings
- Business units (brands) are read-only through the API.Source pending
- Without multi-currency,
GET …/currencies/company-currency(both2026-09andv3) returns 404 with an empty body. The account details (GET /account-info/v3/details) carrycompanyCurrency.Measured - The users API has no login field; last login comes from
GET /account-info/{v}/activity/login, one row per attempt withloginAtandloginSucceeded, Enterprise only, covering the last 90 days.Documented and measured GET /crm/v3/ownersreturns users who were invited but have not accepted yet, so they can be set as record owners before the invite is accepted.Measured- Roles (permission sets) are read-only through the API.Source pending
- Sandboxes and developer test accounts have no REST API for creation; they are created with the HubSpot CLI (
hs sandbox,hs test-account).Source pending - Teams can be created, renamed and given members only through the
2026-09Teams API (/settings/teams/2026-09); no legacy path writes teams.Documented and measured - Creating a user through the API sends a HubSpot invite when
sendWelcomeEmailis true.Source pending GET /settings/v3/usershas no pending or active flag, so the API cannot tell whether an invited user has accepted.Measured
UI
- App card placement on a record has no API; cards are added from the record via Customize → Add card, filtered to "App".Source pending
- There is no public API for the create-record form; it is set in Settings → Objects → [object] → Create Record.Source pending
- There is no public API for the record preview panel; it is set in Record Customization → Preview customization.Source pending
- There is no public API, beta included, for record layouts (tabs, cards, sidebars); they are set in Settings → Data Management → Objects → [object] → Record Customization.Source pending
- There is no public API, beta included, for saved index views; they are added under the object index's view tabs (+ Add view / Manage views).Source pending
- Chatflows, inboxes, knowledge base articles, playbooks, snippets, documents and surveys have no public configuration API; they are set up on their Settings pages.Source pending
Workflows
- In the Automation v4 spec, an
AB_TEST_BRANCHaction routes each path throughtestBranches[].connection.Source pending - The workflow action-type catalog (
GET /automation/2026-09-beta/action-types) carries each type'sactionTypeId,name,groupand input names only. It has no field that says an action is Breeze, uses AI, or consumes credits, so credit-using actions can only be recognized from a curated list of ids.Measured - The action-type catalog
GET /automation/2026-09-beta/action-typesdoesn't list the classic actions0-3(task),0-8(internal email) or0-11(rotate owner), though flows using them save and run.Measured - Add to static list is workflow action
0-63809083at version 5, withfields: { listId, targetObject: "{{ enrolled_object }}" }.Source pending - A multi-checkbox "add to existing values" edit (
0-5) isvalue: { type: APPEND_OBJECT_PROPERTY, appendPropertyName }, and it works together withassociation. A guessedfields.appendValueis silently dropped.Measured - Creating a flow that combines if/then branches with copy-to-associated-record actions (
0-5withassociation) failed in every variant tried (400 or 500). Linear flows with the same actions saved.Measured - These 15 workflow action types in the
GET /automation/2026-09-beta/action-typescatalog look like Breeze (AI) actions:0-218870689(Run custom agent),0-218870680(Use a custom LLM),0-216647524,0-201091202and0-172351286(Data Agent: Research, Fill Smart Property, Custom prompt),0-229131793(Assign to Customer Agent),0-219292676and0-217709844(Enroll in, Unenroll from prospecting agent),0-219160146,0-270204748,0-219161394and0-270226538(Track, Stop tracking intent signals and custom signals),0-195318603(Summarize record),0-207702619(Infer company value proposition and ICP) and0-207702621(Research company news). They were picked by name on a contacts catalog of 68 types; which of them HubSpot bills as a credit-consuming Breeze workflow action isn't confirmed.Measured - Creating a flow whose custom code action names a secret the portal doesn't have returns HTTP 500. A secret attached in the workflow editor survives a later GET then PUT of the flow.Measured
- A contact workflow must be
type: CONTACT_FLOW;PLATFORM_FLOWwith object0-1returns 400EXPECTED_PLATFORM_OBJECT_TYPE. Custom-object workflows usePLATFORM_FLOW.Measured - Edit record (
0-5) writes to the enrolled record's associated records when its fields includeassociation: { associationCategory, associationTypeId }, using the type ID from the enrolled record's side.Measured - The create-record action (
0-14) copies a value from the enrolled record withproperties: [{ targetProperty, value: { type: OBJECT_PROPERTY, propertyName } }]. Its association to the enrolled record uses the association type seen from the new record's side.Measured - A custom-event trigger is an
EVENT_BASEDeventFilterBranchesentry{ filterBranchType: UNIFIED_EVENTS, eventTypeId: "6-…", operator: HAS_COMPLETED }, and it works on a custom-object workflow. Several unfiltered event branches are OR-ed.Measured - The delay action is
0-1withfields: { delta, time_unit }, wheredeltais a string andtime_unitisMINUTES,HOURSorDAYS.Source pending - Workflow filters and actions on a dropdown property take the option's internal value, not its label. A label is accepted without an error, then matches or writes nothing, and a later enable can fail with a generic 400.Measured
- The marketing email click event type ID in workflow triggers is
4-666288.Measured - Turning a list-based workflow on through the API, or changing its enrollment conditions, does not enroll records that already qualify; only records that start to qualify afterwards enroll.Measured
- An
EVENT_BASEDworkflow with a property-change trigger that also sendslistMembershipFilterBranchesis accepted, but the filters are silently dropped: a GET returnslistMembershipFilterBranches: []. Measured on a custom-object workflow.Measured - A workflow action reaches a record associated with the enrolled one through a flow
dataSourcesentry{ name, type: ASSOCIATION, objectTypeId, associationTypeId, associationCategory }and the token{{ fetched_objects.<name> }}.Measured - The form-submission event type ID in workflow triggers is
4-1639801; an unfilteredUNIFIED_EVENTSbranch on it enrolls on any form.Measured - Every flow
connection, list branch, static branch anddefaultBranchcarriesedgeTypeSTANDARDorGOTO;GOTOmarks a rejoin to a shared action, and the last action has noconnection.Measured - On create, HubSpot adds a flow
dataSourcesentry{ type: ASSOCIATION, objectTypeId, associationTypeId, associationCategory, name: "ds_<hex>" }for every associated-record action, so a create should not send those entries itself.Measured - "Increase or decrease property value" is action
0-261589386withfields: { targetObject: "{{ enrolled_object }}", targetProperty, numericValue }. HubSpot stores it as version 1, readsnumericValueback as a string, and after an edit in the workflow editortargetPropertyreads<objectTypeId>/<property>.Measured - Remove association label
0-61139476(v12) takesfromObjectCoordinates,toObjectType,labelToRemove(<CATEGORY>_<typeId>) andfullyDissociate. Apply label by matching property0-73444249(v7) takeslabelToApply,matchBy: fromAndToObjectsand the two…PropertyNameToMatchfields.Measured - Automation v4 has no endpoint to enroll or unenroll records. The legacy v2 endpoint enrolls contacts by email into legacy workflow IDs only, so a custom-object record can be enrolled only by hand or by meeting the trigger.Documented and measured
- The workflow action-type catalog (
/automation/2026-09-beta/action-types) has no "send custom event" action; custom code is0-32and format data0-33.Measured - A property-change start trigger is an
EVENT_BASEDeventFilterBranchesentry withfilterBranchType: UNIFIED_EVENTS,eventTypeId: "4-655002",operator: HAS_COMPLETEDand anhs_name IS_EQUAL_TO <property>filter. It re-enrolls the same record on every change, custom objects included.Measured - An action a flow PUT adds, or re-adds, must have an
actionIdat or above the flow's currentnextAvailableActionId; reusing an older or lower ID returns 400 "Invalid request to flow update". Renumber fromnextAvailableActionIdinstead.Measured - A PUT that adds an edit-record action with
association(0-5) to an existing flow is refused with 400 FLOW_UPDATE_BAD_REQUEST, even with a body that saves on create. A PUT on a flow created with such actions works.Measured - A workflow update (
PUT /automation/v4/flows/{flowId}) replaces the whole flow and must carry its currentrevisionId: GET, change, PUT. A stale revision is refused and changes nothing, and anything missing from the body is lost.Documented and measured - A re-enrollment trigger (for example "phone changed") only fires for records that were enrolled in the workflow at least once before.Measured
- A list-based workflow re-enrolls on a property change through a
reEnrollmentTriggersFilterBranchesAND branch withhs_name STRING IS_EQUAL_TO <property>and anhs_valuefilter:ALL_PROPERTY IS_KNOWNfor any change,BOOL IS_EQUAL_TO truefor "changes to true".Measured - A
UNIFIED_EVENTSproperty-change branch (4-655002) in a list-based flow'sreEnrollmentTriggersFilterBranchesis accepted but never fires, and the workflow editor shows that re-enrollment trigger unchecked.Measured - A flow's
revisionIdmoves by exactly one on each API edit tested (rename, enable, disable, action replace), and the list and detail reads agree on it. UI edits and no-op PUTs are untested.Measured - Rotate-owner (
0-11) assigns only users with an accepted invite and a seat that can own records. Other users, such as view-only users, are skipped without an error: the step runs and the owner stays empty.Measured - The rotate-owner workflow action (
0-11) takes teams asteam_ids: ["<team id>"]. HubSpot doesn't validate the team IDs: a nonexistent ID is accepted.Measured - Rotate-owner (
0-11) takes users asuser_ids: [...]andoverwrite_current_owneras a string ("false"). An owner field cleared to an empty string counts as no owner, and rotation assigns one.Measured - Exactly one
edgeType: STANDARDconnection may enter a workflow action; every other connection into it must beGOTO. A flow with several STANDARD edges into one action fails with "Invalid request to flow creation".Measured - In a workflow task action (
0-3), eachassociations[].value.typemust beENROLLED_OBJECTorCOPY_ASSOCIATION;COPY_ASSOCIATIONcarries asourceSpec(the enrolled record to X association). Any other value returns "Invalid set association value type".Measured - A task's
due_time: { delta: 1, timeUnit: DAYS, timeOfDay: { hour: 9, minute: 0 }, daysOfWeek: [MONDAY…FRIDAY] }makes it due the next business day at 09:00 in the portal's time zone.Measured - A create-task action can take its owner from the enrolled record with
owner_assignment: {type: CUSTOM, value: {type: OBJECT_PROPERTY, propertyName: hubspot_owner_id}}; it reads the owner set by a rotate step earlier in the same run.Measured {{ enrolled_object.<property> }}tokens in a create-task (0-3) subject or body render on a custom-object workflow, and a dropdown property's token renders the option label, not the internal value.Measured- Trigger-only edits, including changed enrollment conditions, can be PUT on an enabled workflow when no associated-record action changes.Measured
- A typed value such as
{ type: STATIC_VALUE, staticValue }given for a plain-string workflow action field is accepted and stored as its literal text ({type=STATIC_VALUE, staticValue=…}), so the action does the wrong thing without an error.Measured - The Automation v4 workflows API (
/automation/v4/flows) is still beta.Source pending - In the Automation v4 spec, the webhook action (
ApiWebhookAction) carrieswebhookUrl,queryParamsandauthSettings.Source pending