- 
                Notifications
    You must be signed in to change notification settings 
- Fork 5.5k
18029 actionhubspot cms and marketing api #18155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
18029 actionhubspot cms and marketing api #18155
Conversation
- Added new actions: Create Contact Workflow, Clone Email, Clone Site Page, Create Form, Update Landing Page, and Update Page. - Enhanced existing actions with version increments for better compatibility. - Introduced utility functions for improved data handling and object parsing. - Updated dependencies in package.json to the latest versions. - Refactored code for consistency and clarity across various actions and sources.
| The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
 | 
| WalkthroughAdds HubSpot CMS and Marketing capabilities: new actions for forms, pages, landing pages, emails, workflows; shared page props and cleanObject utility; LANGUAGE_OPTIONS and API_PATH update; major hubspot.app additions (new methods, propDefinitions, Bottleneck rate limiter); one CRM association tweak; many version bumps and import reorderings. Changes
 Sequence Diagram(s)sequenceDiagram
  autonumber
  actor User
  participant Action as CMS/Marketing Action
  participant App as HubSpot App
  participant RL as Bottleneck Rate Limiter
  participant API as HubSpot API
  User->>Action: invoke action with props
  Action->>App: call create/update/clone method (payload, $)
  App->>RL: schedule request
  RL->>API: HTTP request
  API-->>RL: response
  RL-->>App: response
  App-->>Action: response data
  Action-->>User: $.export("$summary") + return
  note right of Action: parseObject / cleanObject applied where used
sequenceDiagram
  autonumber
  actor User
  participant Action as Create Contact Workflow
  participant App as HubSpot App
  participant API as HubSpot Automation v4
  User->>Action: configure workflow props
  Action->>App: createContactWorkflow(data)
  App->>API: POST /automation/v4/flows
  API-->>App: { id, ... }
  App-->>Action: response
  Action-->>User: "$summary: created workflow {id}"
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Assessment against linked issues
 Assessment against linked issues: Out-of-scope changes
 Possibly related PRs
 Suggested reviewers
 Poem
 Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. 📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration: 
 You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (1)
 ✅ Files skipped from review due to trivial changes (1)
 ⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
 ✨ Finishing Touches
 🧪 Generate unit tests
 Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit: 
 SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type  Other keywords and placeholders
 CodeRabbit Configuration File ( | 
…e-companies to 0.0.3 and new-email-event to 0.0.29 for improved compatibility.
- Added dependencies for components/prisma_management_api. - Cleaned up specifier entries for components/mindee and components/zapr_link. - Included transitive peer dependencies for better compatibility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 23
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️  Outside diff range comments (31)
components/hubspot/sources/new-company-property-change/new-company-property-change.mjs (1)
49-79: Guard the date filter whenafteris undefined and coerce it to a stringOn first runs,
afterwill beundefined, and sending{ propertyName: "hs_lastmodifieddate", operator: "GTE", value: undefined }can lead to CRM Search errors. The Search API also expects filter values as strings. In components/hubspot/sources/new-company-property-change/new-company-property-change.mjs (lines 49–79), change
getParamsto:getParams(after) { - return { - object: "companies", - data: { - limit: DEFAULT_LIMIT, - properties: [ this.property ], - sorts: [{ propertyName: "hs_lastmodifieddate", direction: "DESCENDING" }], - filterGroups: [ - { - filters: [ - { propertyName: this.property, operator: "HAS_PROPERTY" }, - { - propertyName: "hs_lastmodifieddate", - operator: "GTE", - value: after, - }, - ], - }, - ], - }, - }; - }, + // Always require the property, but only filter by date if `after` is defined + const filters = [ + { propertyName: this.property, operator: "HAS_PROPERTY" }, + ]; + if (after != null) { + filters.push({ + propertyName: "hs_lastmodifieddate", + operator: "GTE", + value: String(after), // ensure the API gets a string + }); + } + + return { + object: "companies", + data: { + limit: DEFAULT_LIMIT, + properties: [ this.property ], + sorts: [{ propertyName: "hs_lastmodifieddate", direction: "DESCENDING" }], + filterGroups: [{ filters }], + }, + }; + },
- Moves the date filter behind an
if (after != null)guard to avoidvalue: undefined- Uses
String(after)to satisfy the API’s requirement for string‐typed filter valuescomponents/hubspot/actions/list-marketing-emails/list-marketing-emails.mjs (1)
81-116: Critical bug:resultsshadowing and self-appending within iteration corrupts pagination and output.
- You declare
const results = [](outer accumulator), then destructure{ paging, results }from the API response, shadowing the accumulator.- Inside
for (const item of results), you push into the sameresultsyou’re iterating, which can extend iteration unexpectedly and never populates the outer accumulator. The returned array will likely be empty and the loop may over-iterate.Fix by using distinct names and pushing into the accumulator; also guard
paging?.next?.afterand only setparams.afterwhen present.Apply this diff:
async run({ $ }) { - - const results = []; - let hasMore, count = 0; + const allResults = []; + let hasMore; + let count = 0; @@ - do { - const { - paging, results, - } = await this.hubspot.listMarketingEmails({ + do { + const { paging, results: pageResults } = await this.hubspot.listMarketingEmails({ $, params, }); - if (!results?.length) { + if (!pageResults?.length) { break; } - for (const item of results) { - results.push(item); + for (const item of pageResults) { + allResults.push(item); count++; if (count >= this.maxResults) { break; } } - hasMore = paging?.next.after; - params.after = paging?.next.after; + hasMore = Boolean(paging?.next?.after); + if (hasMore) { + params.after = paging.next.after; + } } while (hasMore && count < this.maxResults); - $.export("$summary", `Found ${results.length} email${results.length === 1 + $.export("$summary", `Found ${allResults.length} email${allResults.length === 1 ? "" : "s"}`); - return results; + return allResults; },components/hubspot/actions/list-blog-posts/list-blog-posts.mjs (2)
75-105: Fix results shadowing and mutation bug — function always returns [] and may duplicate items.
- Outer accumulator
results(Line 75) is shadowed by the destructuredresultsfrom the API (Line 91).- Inside the loop you push into the API’s
resultswhile iterating it, not the accumulator (Lines 99-101). This both duplicates items and leaves the outerresultsempty, so the action returns[].Apply this minimal fix:
- const results = []; + const items = []; let hasMore, count = 0; @@ - const { - paging, results, - } = await this.hubspot.getBlogPosts({ + const { + paging, results: apiResults, + } = await this.hubspot.getBlogPosts({ $, params, }); - if (!results?.length) { + if (!apiResults?.length) { break; } - for (const item of results) { - results.push(item); + for (const item of apiResults) { + items.push(item); count++; if (count >= this.maxResults) { break; } } - hasMore = paging?.next.after; - params.after = paging?.next.after; + hasMore = Boolean(paging?.next?.after); + params.after = paging?.next?.after; } while (hasMore && count < this.maxResults); @@ - $.export("$summary", `Found ${results.length} page${results.length === 1 + $.export("$summary", `Found ${items.length} blog post${items.length === 1 ? "" : "s"}`); - return results; + return items;
53-65: Action Required: Alignsort.optionswith HubSpot’s CMS Blog Posts API property namesThe
sortparameter itself is correct—HubSpot expects asortquery parameter with the property name (optionally prefixed by-for descending) (developers.hubspot.com). However, the code’s list of allowed fields must exactly match the property keys exposed by the CMS Blog Posts API:
- Replace
createdBy→createdById- Replace
updatedBy→updatedByIdAdditionally, if you wish to support sorting by publish date (a common use case), consider adding
publishDateto the options.Please update the snippet in
components/hubspot/actions/list-blog-posts/list-blog-posts.mjsaccordingly:sort: { type: "string", label: "Sort", description: "Sort the results by the specified field", - options: [ - "name", - "createdAt", - "updatedAt", - "createdBy", - "updatedBy", - ], + options: [ + "name", + "createdAt", + "updatedAt", + "createdById", + "updatedById", + // Optional: sort by publish date + "publishDate", + ], optional: true, },components/hubspot/actions/list-campaigns/list-campaigns.mjs (1)
34-66: Fix variable shadowing and self-appending loop that corrupts results
- The outer
resultsarray is shadowed by the destructured{ results }from the API response.- Inside the loop, you push into the same array you’re iterating (
results.push(item)), duplicating entries and risking pathological iteration. The outerresultsstays empty, causing an empty return and summary.Refactor to separate page results from the accumulator and handle paging defensively.
Apply this diff:
- const results = []; - let hasMore, count = 0; + const campaigns = []; + let after; + let count = 0; @@ - const params = { - sort: this.sort, - }; + const params = { sort: this.sort }; @@ - const { - paging, results, - } = await this.hubspot.listCampaigns({ + const { + paging, results: page, + } = await this.hubspot.listCampaigns({ $, params, }); - if (!results?.length) { + if (!page?.length) { break; } - for (const item of results) { - results.push(item); + for (const item of page) { + campaigns.push(item); count++; if (count >= this.maxResults) { break; } } - hasMore = paging?.next.after; - params.after = paging?.next.after; - } while (hasMore && count < this.maxResults); + after = paging?.next?.after; + if (after) params.after = after; + else delete params.after; + } while (after && count < this.maxResults); @@ - $.export("$summary", `Found ${results.length} campaign${results.length === 1 + $.export("$summary", `Found ${campaigns.length} campaign${campaigns.length === 1 ? "" : "s"}`); - return results; + return campaigns;components/hubspot/actions/get-file-public-url/get-file-public-url.mjs (1)
31-35: Guard against undefined file before dereferencing idIf
findreturnsundefined, accessingfile.idthrows before your explicit not-found check. Validatefilefirst.Apply this diff:
- const file = files.find(({ url }) => url === fileUrl ); - const fileId = file.id; - if (!fileId) { - throw new Error(`File not found at ${fileUrl}`); - } + const file = files.find(({ url }) => url === fileUrl); + if (!file) { + throw new Error(`File not found at ${fileUrl}`); + } + const fileId = file.id;components/hubspot/actions/batch-create-or-update-contact/batch-create-or-update-contact.mjs (3)
28-44: Potential crash when no results are returned and undefined emails are includedTwo issues:
- Line 28 collects
undefinedcan be sent to the IN filter.- Line 42 sets
updateEmails = results?.map(...), which becomesundefinedwhenresultsis falsy, leading toupdateEmails.includes(...)throwing at Line 43.Patch below makes this path safe with minimal change.
- const emails = contacts.map(({ email }) => email); + const emails = contacts.map(({ email }) => email).filter(Boolean); @@ - const updateEmails = results?.map(({ properties }) => properties.email); - const insertProperties = contacts.filter(({ email }) => !updateEmails.includes(email)) + const updateEmails = (results ?? []).map(({ properties }) => properties.email).filter(Boolean); + const insertProperties = contacts.filter(({ email }) => !updateEmails.includes(email)) .map((properties) => ({ properties, }));
27-58: Partition create/update correctly, dedupe updates, and validate inputsCurrent flow risks:
- Contacts with an
idcan be pushed into both create and update arrays (due to only filtering by email for creates) causing duplicate/conflicting requests.- Update set may contain duplicates when mixing “found by email” and “provided id” paths.
- Records without
idand withoutRefactor:
- Validate each contact has either
id(update) or- Partition by presence of
id.- Search only the “no id but has email” cohort, compute inserts (non-existing emails) and updates (existing emails).
- Merge updates from “by email” and “by id”, deduping by
id(id-driven updates take precedence).Apply this diff:
@@ - async searchExistingContactProperties(contacts, $) { - const emails = contacts.map(({ email }) => email).filter(Boolean); - const { results } = await this.hubspot.searchCRM({ - $, - object: "contact", - data: { - filters: [ - { - propertyName: "email", - operator: "IN", - values: emails, - }, - ], - }, - }); - const updateEmails = (results ?? []).map(({ properties }) => properties.email).filter(Boolean); - const insertProperties = contacts.filter(({ email }) => !updateEmails.includes(email)) - .map((properties) => ({ - properties, - })); - const updateProperties = []; - for (const contact of results) { - updateProperties.push({ - id: contact.id, - properties: contacts.find(({ email }) => contact.properties.email === email), - }); - } - return { - insertProperties, - updateProperties, - }; - }, + async searchExistingContactProperties(contacts, $) { + // Consider only contacts that do NOT have an id and DO have an email + const candidates = contacts.filter((c) => !Object.prototype.hasOwnProperty.call(c, "id") && c.email); + const emails = [...new Set(candidates.map(({ email }) => email).filter(Boolean))]; + if (!emails.length) { + return { insertProperties: [], updateProperties: [] }; + } + const { results = [] } = await this.hubspot.searchCRM({ + $, + object: "contact", + data: { + filters: [ + { + propertyName: "email", + operator: "IN", + values: emails, + }, + ], + }, + }); + const existingEmails = new Set(results.map(({ properties }) => properties.email).filter(Boolean)); + const insertProperties = candidates + .filter(({ email }) => !existingEmails.has(email)) + .map((properties) => ({ properties })); + const byEmail = new Map(candidates.map((c) => [c.email, c])); + const updateProperties = results.map(({ id, properties }) => ({ + id, + properties: byEmail.get(properties.email), + })); + return { insertProperties, updateProperties }; + }, @@ - const { - insertProperties, updateProperties, - } = await this.searchExistingContactProperties(contacts, $); + // Validate minimal required identifiers + const invalid = contacts.filter((c) => + !Object.prototype.hasOwnProperty.call(c, "id") && !c.email); + if (invalid.length) { + throw new Error("Each contact must include either an 'id' (for update) or an 'email' (for create/update)."); + } + const { insertProperties, updateProperties } = + await this.searchExistingContactProperties(contacts, $); @@ - const updatePropertiesWithId = contacts.filter((contact) => (Object.prototype.hasOwnProperty.call(contact, "id"))) - .map(({ - id, ...properties - }) => ({ - id: id, - properties, - })); - - if (updatePropertiesWithId?.length) { - updateProperties.push(...updatePropertiesWithId); - } + const updatesById = contacts + .filter((c) => Object.prototype.hasOwnProperty.call(c, "id")) + .map(({ id, ...properties }) => ({ id, properties })); + // Merge and dedupe updates by id (id-driven updates take precedence) + const mergedUpdateMap = new Map(updateProperties.map((u) => [String(u.id), u])); + for (const u of updatesById) mergedUpdateMap.set(String(u.id), u); + const mergedUpdates = Array.from(mergedUpdateMap.values()); @@ - response.created = await this.hubspot.batchCreateContacts({ - $, - data: { - inputs: insertProperties, - }, - }); - response.updated = await this.hubspot.batchUpdateContacts({ - $, - data: { - inputs: updateProperties, - }, - }); + response.created = await this.hubspot.batchCreateContacts({ + $, + data: { inputs: insertProperties }, + }); + response.updated = await this.hubspot.batchUpdateContacts({ + $, + data: { inputs: mergedUpdates }, + }); @@ - $.export("$summary", `Successfully created ${insertProperties.length} and updated ${updateProperties.length} contacts`); + $.export("$summary", `Successfully created ${insertProperties.length} and updated ${mergedUpdates.length} contacts`);Also applies to: 61-77, 79-96
79-91: Implement chunking for HubSpot batch endpointsThe
batchCreateContactsandbatchUpdateContactsmethods inhubspot.app.mjsdo not enforce the 100-item limit; they simply pass through youroptsas-is. To avoid request failures wheninsertPropertiesorupdatePropertiesexceeds HubSpot’s 100-item cap, add client-side chunking in this action:• In
components/hubspot/actions/batch-create-or-update-contact/batch-create-or-update-contact.mjs(around lines 79–91), wrap each call in a loop that:
- Splits the full
inputsarray into subarrays of at most 100 entries.- Calls
this.hubspot.batchCreateContacts(orbatchUpdateContacts) for each chunk.- Aggregates the individual responses into a combined
response.created/response.updated.• You can implement a small helper in this module, for example:
const CHUNK_SIZE = 100; function chunkArray(arr, size) { const chunks = []; for (let i = 0; i < arr.length; i += size) { chunks.push(arr.slice(i, i + size)); } return chunks; } // then in your action: response.created = []; for (const chunk of chunkArray(insertProperties, CHUNK_SIZE)) { const res = await this.hubspot.batchCreateContacts({ $, data: { inputs: chunk } }); response.created.push(...res.results); }This will ensure you never send more than 100 contacts per request and handle arbitrary-sized payloads gracefully.
components/hubspot/sources/new-engagement/new-engagement.mjs (1)
19-19: Typo in user-facing description (“engagment”)Fix the typo to avoid user confusion in the UI.
- description: "Filter results by the type of engagment", + description: "Filter results by the type of engagement",components/hubspot/actions/list-marketing-events/list-marketing-events.mjs (1)
26-45: Critical: variable shadowing and self-appending during iteration corrupt resultsInside the loop:
- You destructure
resultsfrom the API response, which shadows the outerresultsaccumulator.- You then
for ... of resultsand push toresultsinside the same loop. This mutates the array being iterated and, combined with shadowing, results in incorrect accumulation (and can lead to unexpected iteration growth). The outer accumulator remains empty, so the function returns[]and the summary reports0.Fix by separating page results from the accumulator and pushing into the accumulator only.
- const results = []; + const events = []; @@ - const { - paging, results, - } = await this.hubspot.listMarketingEvents({ + const { + paging, results: pageResults, + } = await this.hubspot.listMarketingEvents({ $, params, }); - if (!results?.length) { + if (!pageResults?.length) { break; } - for (const item of results) { - results.push(item); + for (const item of pageResults) { + events.push(item); count++; if (count >= this.maxResults) { break; } } hasMore = paging?.next.after; params.after = paging?.next.after; } while (hasMore && count < this.maxResults); @@ - $.export("$summary", `Found ${results.length} event${results.length === 1 + $.export("$summary", `Found ${events.length} event${events.length === 1 ? "" : "s"}`); - return results; + return events;Also applies to: 20-24, 47-51
components/hubspot/actions/get-associated-meetings/get-associated-meetings.mjs (1)
192-202: Custom timeframe filter operators are invertedFor a custom range, start should be GTE
startDateand end should be LTEendDate. Current logic uses the opposite, which will return incorrect/empty results.case "custom": return { hs_meeting_start_time: { - operator: "LTE", - value: startDate, + operator: "GTE", + value: startDate, }, hs_meeting_end_time: { - operator: "GTE", - value: endDate, + operator: "LTE", + value: endDate, }, };components/hubspot/sources/new-task/new-task.mjs (2)
36-41: Fix: spreading an undefined value throws at runtime.If listSchemas() returns an object without a results array, custom becomes undefined and
...customObjectswill throw. Default to an empty array.- const { results: custom } = await this.hubspot.listSchemas(); - const customObjects = custom?.map(({ fullyQualifiedName }) => fullyQualifiedName); + const { results: custom = [] } = await this.hubspot.listSchemas(); + const customObjects = custom.map(({ fullyQualifiedName }) => fullyQualifiedName);
52-53: Use correct binding context forhubspot.listTasksVerified that
listTasksinhubspot.app.mjsinvokesthis.makeRequest(line 1265), so binding it to the integration component (this) will break its context. It must be bound to the HubSpot client instance instead.• File:
components/hubspot/sources/new-task/new-task.mjs(around lines 52–53)
• Replace with:- const tasks = await this.getPaginatedItems(this.hubspot.listTasks.bind(this), params); + const tasks = await this.getPaginatedItems(this.hubspot.listTasks.bind(this.hubspot), params);components/hubspot/sources/new-deal-property-change/new-deal-property-change.mjs (1)
27-33: Fix: robust timestamp handling for property history.HubSpot property history timestamps are often epoch milliseconds (number).
Date.parse(number)returns NaN. Handle both number and string to prevent NaN timestamps propagating to dedupe logic and meta.- getTs(deal) { - const history = deal.propertiesWithHistory[this.property]; - if (!history || !(history.length > 0)) { - return; - } - return Date.parse(history[0].timestamp); - }, + getTs(deal) { + const history = deal.propertiesWithHistory?.[this.property]; + if (!history?.length) return; + const t = history[0]?.timestamp; + const ts = typeof t === "number" ? t : Date.parse(t); + return Number.isFinite(ts) ? ts : undefined; + },components/hubspot/sources/new-note/new-note.mjs (2)
36-41: Fix: spreading an undefined value throws at runtime.Mirror the task source fix—default
resultsto [] to keep...customObjectssafe.- const { results: custom } = await this.hubspot.listSchemas(); - const customObjects = custom?.map(({ fullyQualifiedName }) => fullyQualifiedName); + const { results: custom = [] } = await this.hubspot.listSchemas(); + const customObjects = custom.map(({ fullyQualifiedName }) => fullyQualifiedName);
52-53: BindlistNotesto the HubSpot client contextThe
listNotesimplementation invokesthis.makeRequest, so binding it to the component’sthiswill break its internalthisreference. Update the call incomponents/hubspot/sources/new-note/new-note.mjsaccordingly:• File: components/hubspot/sources/new-note/new-note.mjs
• Lines: 52–53- const notes = await this.getPaginatedItems(this.hubspot.listNotes.bind(this), params); + const notes = await this.getPaginatedItems(this.hubspot.listNotes.bind(this.hubspot), params); await this.processEvents(notes, after);components/hubspot/actions/create-meeting/create-meeting.mjs (2)
83-85: Tighten validation when association fields are partially filledIf either association field is provided, require both and a target type. This prevents confusing 400s from HubSpot.
Apply this diff:
- if ((toObjectId && !associationType) || (!toObjectId && associationType)) { - throw new ConfigurationError("Both `toObjectId` and `associationType` must be entered"); - } + if ((toObjectId && !associationType) || (!toObjectId && associationType)) { + throw new ConfigurationError("Both `toObjectId` and `associationType` must be entered"); + } + if ((toObjectId || associationType) && !toObjectType) { + throw new ConfigurationError("`toObjectType` is required when associating a meeting."); + }
100-105: DeriveassociationCategorydynamically instead of hard-codingHUBSPOT_DEFINEDThe current implementation always forces
associationCategory: ASSOCIATION_CATEGORY.HUBSPOT_DEFINED, which will break for USER_DEFINED or INTEGRATOR_DEFINED types.Key locations to update:
- components/hubspot/hubspot.app.mjs (around line 369)
TheassociationTypepropDefinition returns only an integer (typeId) as the value—and nocategoryfield—so downstream code can’t know the correct category.- components/hubspot/actions/create-meeting/create-meeting.mjs (lines 100–105)
Thetypesarray is still hard-coded toHUBSPOT_DEFINED.You should refactor one of two ways:
Extend the prop to return both
idandcategory:
Incommon.props.hubspot.associationType.options, map each type toreturn associationTypes.map((t) => ({ label: t.label, - value: t.typeId, + value: { id: t.typeId, category: t.category }, }));Then update your
create-meetingaction to unpack that object.
Fetch the category in the action itself:
const { results } = await this.hubspot.getAssociationTypes({ fromObjectType, toObjectType }); const assoc = results.find(({ typeId }) => typeId === associationType); // … types: [ { associationTypeId: assoc.typeId, associationCategory: assoc.category, }, ],Finally, apply this diff in
create-meeting.mjsto replace the hard-coded snippet:- types: [ - { - associationTypeId: associationType, - associationCategory: ASSOCIATION_CATEGORY.HUBSPOT_DEFINED, - }, - ], + types: [ + (() => { + // derive id/category from the prop or by calling getAssociationTypes + const assoc = /* … */; + return { + associationTypeId: assoc.id, + associationCategory: assoc.category, + }; + })(), + ],This change is critical: forcing
HUBSPOT_DEFINEDfor user- or integrator-defined types will cause API errors. Please address before merging.components/hubspot/actions/create-lead/create-lead.mjs (1)
35-55: Hardcoded associationTypeId and unconditional association can cause failures or brittleness.
- The magic number 578 can change or vary; hardcoding is brittle.
- If contactId is absent/invalid, the API call will fail.
- Overwrites any associations provided in opts.data instead of merging.
Refactor to (a) merge with any provided associations, (b) guard on contactId, (c) cast id to string, and (d) use a named constant.
Apply this diff:
createObject(opts) { - return this.hubspot.createObject({ - ...opts, - data: { - ...opts?.data, - associations: [ - { - types: [ - { - associationCategory: ASSOCIATION_CATEGORY.HUBSPOT_DEFINED, - associationTypeId: 578, // ID for "Lead with Primary Contact" - }, - ], - to: { - id: this.contactId, - }, - }, - ], - }, - }); + const incomingAssociations = opts?.data?.associations ?? []; + const associations = [...incomingAssociations]; + + if (this.contactId) { + associations.push({ + types: [ + { + associationCategory: ASSOCIATION_CATEGORY.HUBSPOT_DEFINED, + associationTypeId: ASSOCIATION_TYPE_ID.LEAD_WITH_PRIMARY_CONTACT, + }, + ], + to: { + id: String(this.contactId), + }, + }); + } + + return this.hubspot.createObject({ + ...opts, + data: { + ...opts?.data, + associations, + }, + }); },Add this constant to your HubSpot constants (see next snippet), or alternatively resolve the association type dynamically at runtime via the Associations API.
components/hubspot/actions/list-pages/list-pages.mjs (1)
75-105: Critical: variable shadowing corrupts results and can trigger an unbounded loop.
- Outer results array is shadowed by the destructured results from the API response.
- You then push into the array you’re iterating (inner results), potentially extending iteration indefinitely until maxResults, and you never populate the outer results, so you return [].
Fix by renaming one side and pushing into the outer accumulator. Also ensure hasMore is boolean.
Apply this diff:
- const results = []; + const pages = []; @@ - const { - paging, results, - } = await this.hubspot.listPages({ + const { + paging, results: pageResults, + } = await this.hubspot.listPages({ $, params, }); - if (!results?.length) { + if (!pageResults?.length) { break; } - for (const item of results) { - results.push(item); + for (const item of pageResults) { + pages.push(item); count++; if (count >= this.maxResults) { break; } } - hasMore = paging?.next.after; + hasMore = Boolean(paging?.next?.after); params.after = paging?.next.after; } while (hasMore && count < this.maxResults); @@ - $.export("$summary", `Found ${results.length} page${results.length === 1 + $.export("$summary", `Found ${pages.length} page${pages.length === 1 ? "" : "s"}`); - return results; + return pages;components/hubspot/actions/list-forms/list-forms.mjs (2)
26-52: Fix variable shadowing and self-appending bug; results are never accumulated and loop can degenerate.
const results = [](accumulator) is shadowed by the destructured{ results }from the API response.- Inside the
for...of, you push to the response array you’re iterating instead of the accumulator, which can cause growth during iteration and incorrect behavior. The outer accumulator remains empty.Apply this diff to use distinct names and push into the correct accumulator:
- const results = []; - let hasMore, count = 0; + const items = []; + let after; + let count = 0; @@ - const params = { - archived: this.archived, - }; + const params = { + archived: this.archived, + }; @@ - const { - paging, results, - } = await this.hubspot.listMarketingForms({ + const { + paging, results: pageResults, + } = await this.hubspot.listMarketingForms({ $, params, }); - if (!results?.length) { + if (!pageResults?.length) { break; } - for (const item of results) { - results.push(item); + for (const item of pageResults) { + items.push(item); count++; if (count >= this.maxResults) { break; } } - hasMore = paging?.next.after; - params.after = paging?.next.after; - } while (hasMore && count < this.maxResults); + after = paging?.next?.after; + params.after = after; + } while (after && count < this.maxResults);
54-58: Return and summary currently reference the wrong variable (always 0).Update to use the accumulator from the fix above:
- $.export("$summary", `Found ${results.length} form${results.length === 1 + $.export("$summary", `Found ${items.length} form${items.length === 1 ? "" : "s"}`); - return results; + return items;components/hubspot/actions/create-associations/create-associations.mjs (1)
69-80: Guard against missing association and use the argument value (notthis.*).
results.find(...)may returnundefined, causingassociation.categoryto throw. Also, prefer the passedassociationTypearg overthis.associationTypeinside the method.async getAssociationCategory({ $, fromObjectType, toObjectType, associationType, }) { const { results } = await this.hubspot.getAssociationTypes({ $, fromObjectType, toObjectType, associationType, }); - const association = results.find(({ typeId }) => typeId === this.associationType); - return association.category; + const association = results?.find(({ typeId }) => typeId === associationType); + if (!association) { + throw new ConfigurationError("Association type not found for the selected object types."); + } + return association.category; },components/hubspot/actions/batch-create-companies/batch-create-companies.mjs (1)
42-45: Make error parsing robust; current logic can throw and hide the real error.
JSON.parse(JSON.parse(error.message).message)is brittle. Prefer safe parsing and fallbacks toerror.response.data.- } catch (error) { - const message = JSON.parse((JSON.parse(error.message).message).split(/:(.+)/)[1])[0].message; - throw new ConfigurationError(message.split(/:(.+)/)[0]); - } + } catch (error) { + const fallback = error?.message || "Failed to create companies."; + let details = fallback; + try { + // Try message -> nested message -> array of errors + const parsed1 = typeof error?.message === "string" ? JSON.parse(error.message) : null; + const parsed2 = parsed1 && typeof parsed1.message === "string" ? JSON.parse(parsed1.message) : null; + details = Array.isArray(parsed2) + ? parsed2.map((e) => e?.message).filter(Boolean).join("; ") + : parsed1?.message || fallback; + } catch { + const data = error?.response?.data; + details = data?.errors?.map((e) => e?.message).filter(Boolean).join("; ") + || data?.message + || fallback; + } + throw new ConfigurationError(details); + }components/hubspot/actions/batch-upsert-companies/batch-upsert-companies.mjs (2)
46-48: Brittle error parsing can throw secondary JSON.parse errors; prefer robust extraction from error.response.data.Current approach double-parses strings and splits on colons, which breaks on non-JSON messages and hides original errors. Extract directly from Axios-style response where available, with safe fallbacks.
Apply this diff:
- const message = JSON.parse((JSON.parse(error.message).message).split(/:(.+)/)[1])[0].message; - throw new ConfigurationError(message.split(/:(.+)/)[0]); + const data = error?.response?.data; + const details = Array.isArray(data?.errors) + ? data.errors.map((e) => e?.message).filter(Boolean) + : []; + const base = [data?.message, ...details].filter(Boolean).join(" | "); + const msg = base || error?.message || "Unknown HubSpot error"; + throw new ConfigurationError(msg);
1-50: Extract and Centralize HubSpot Error Parsing LogicI ran the provided search and found the same brittle error-parsing pattern duplicated in three action files. To ensure consistency and simplify future updates, extract this logic into a shared helper (e.g., in
common/utils.mjsor on the HubSpot app) and replace each inline parsing with a call to that helper.Files and locations to refactor:
components/hubspot/actions/batch-upsert-companies/batch-upsert-companies.mjs(lines 46–47)
components/hubspot/actions/batch-update-companies/batch-update-companies.mjs(lines 43–44)
components/hubspot/actions/batch-create-companies/batch-create-companies.mjs(lines 43–44)Suggested steps:
- Create a shared function, e.g.:// common/utils.mjs export function parseHubspotError(error) { // Extract and return the first error message const raw = JSON.parse(error.message); const detail = JSON.parse(raw.message).split(/:(.+)/)[1]; return JSON.parse(detail)[0].message.split(/:(.+)/)[0]; }
- In each action’s
catchblock, replace the inline logic:- const message = JSON.parse((JSON.parse(error.message).message).split(/:(.+)/)[1])[0].message; - throw new ConfigurationError(message.split(/:(.+)/)[0]); + throw new ConfigurationError(parseHubspotError(error));This refactor will reduce duplication and improve maintainability.
components/hubspot/actions/batch-update-companies/batch-update-companies.mjs (1)
43-45: Harden error extraction — avoid double JSON.parse and regex splitting.Mirror the safer approach suggested in batch-upsert to prevent masking the original error.
Apply this diff:
- const message = JSON.parse((JSON.parse(error.message).message).split(/:(.+)/)[1])[0].message; - throw new ConfigurationError(message.split(/:(.+)/)[0]); + const data = error?.response?.data; + const details = Array.isArray(data?.errors) + ? data.errors.map((e) => e?.message).filter(Boolean) + : []; + const base = [data?.message, ...details].filter(Boolean).join(" | "); + const msg = base || error?.message || "Unknown HubSpot error"; + throw new ConfigurationError(msg);components/hubspot/actions/search-crm/search-crm.mjs (3)
83-89: Guard against missing property metadata when building Search Property optionsIf a name listed in
schema.searchablePropertiesisn’t present inschema.properties,propDatawill beundefined, causing a crash when accessing.label. Add a fallback and filter out unresolved props.- const searchableProperties = schema.searchableProperties?.map((prop) => { - const propData = properties.find(({ name }) => name === prop); - return { - label: propData.label, - value: propData.name, - }; - }); + const searchableProperties = schema.searchableProperties + ?.map((prop) => { + const propData = properties.find(({ name }) => name === prop); + return propData + ? { label: propData.label, value: propData.name } + : { label: prop, value: prop }; + }) + .filter(Boolean);
252-257: Validate presence and validity ofsearchPropertybefore calling the APIIf
searchPropertyis undefined, the current message is misleading. Also, guard against schemas that don’t exposesearchableProperties.- if (!schema.searchableProperties.includes(searchProperty)) { + if (!searchProperty) { + throw new ConfigurationError("A Search Property is required for this action."); + } + if (!Array.isArray(schema.searchableProperties) || !schema.searchableProperties.includes(searchProperty)) { throw new ConfigurationError( `Property \`${searchProperty}\` is not a searchable property of object type \`${objectType}\`. ` + `\n\nAvailable searchable properties are: \`${schema.searchableProperties.join("`, `")}\``, ); }
289-293: Make partial-match filtering resilient to non-string property values
toLowerCase()will throw for non-strings (numbers, booleans). Normalize both sides to string before comparison.- if (!exactMatch) { - results = results.filter((result) => - result.properties[searchProperty] - && result.properties[searchProperty].toLowerCase().includes(searchValue.toLowerCase())); - } + if (!exactMatch) { + const needle = String(searchValue ?? "").toLowerCase(); + results = results.filter((result) => { + const raw = result?.properties?.[searchProperty]; + if (raw === undefined || raw === null) return false; + const hay = String(raw).toLowerCase(); + return hay.includes(needle); + }); + }
        
          
                components/hubspot/actions/create-contact-workflow/create-contact-workflow.mjs
          
            Show resolved
            Hide resolved
        
              
          
                components/hubspot/actions/create-contact-workflow/create-contact-workflow.mjs
          
            Show resolved
            Hide resolved
        
              
          
                components/hubspot/actions/create-contact-workflow/create-contact-workflow.mjs
          
            Show resolved
            Hide resolved
        
              
          
                components/hubspot/actions/create-contact-workflow/create-contact-workflow.mjs
          
            Show resolved
            Hide resolved
        
              
          
                components/hubspot/sources/new-or-updated-crm-object/new-or-updated-crm-object.mjs
          
            Show resolved
            Hide resolved
        
              
          
                components/hubspot/sources/new-or-updated-custom-object/new-or-updated-custom-object.mjs
          
            Show resolved
            Hide resolved
        
      Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…page.mjs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @luancazarine lgtm! Ready for QA!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (2)
components/hubspot/common/constants.mjs (2)
382-402: LANGUAGE_OPTIONS imports verified; all use named imports
I confirmed that every occurrence ofLANGUAGE_OPTIONSis correctly imported as a named export in the following files—no stale or default-import usages remain:
- components/hubspot/actions/create-form/create-form.mjs (line 2)
- components/hubspot/actions/update-fields-on-the-form/update-fields-on-the-form.mjs (line 2)
- components/hubspot/actions/clone-email/clone-email.mjs (line 1)
- components/hubspot/actions/common/common-page-prop.mjs (line 1)
You can safely proceed with the export surface changes. As an optional hardening step, consider freezing key constant objects to guard against accidental mutation at runtime. Apply this near their declarations in
components/hubspot/common/constants.mjs:const API_PATH = { PROPERTIES: "/properties/v1", // … COMMUNICATION_PREFERENCES: "/communication-preferences/v4", }; +Object.freeze(API_PATH); const ASSOCIATION_CATEGORY = { HUBSPOT_DEFINED: "HUBSPOT_DEFINED", USER_DEFINED: "USER_DEFINED", INTEGRATOR_DEFINED: "INTEGRATOR_DEFINED", }; +Object.freeze(ASSOCIATION_CATEGORY);
223-380: Updated CI check with correct extraction and duplicate detection using PCRE2’s “\K”:#!/usr/bin/env bash # Extract LANGUAGE_OPTIONS values and detect duplicates # Uses ripgrep with PCRE2 \K to reset match start mapfile -t vals < <(rg -Pno 'value:\s*"\K[^"]+' components/hubspot/common/constants.mjs | sort) if (( ${#vals[@]} == 0 )); then echo "Failed to extract any language values." exit 2 fi # Detect duplicates mapfile -t dups < <(printf '%s\n' "${vals[@]}" | uniq -d) if (( ${#dups[@]} > 0 )); then echo "Duplicate language values found:" printf '%s\n' "${dups[@]}" exit 1 else echo "Extracted ${#vals[@]} values, no duplicates detected." exit 0 fiOnce this script confirms no duplicates, I’d still recommend freezing the array and its entries:
const LANGUAGE_OPTIONS = [ // ... ]; + +// Avoid runtime mutation +for (const o of LANGUAGE_OPTIONS) Object.freeze(o); +Object.freeze(LANGUAGE_OPTIONS);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
- pnpm-lock.yamlis excluded by- !**/pnpm-lock.yaml
📒 Files selected for processing (5)
- components/apify_oauth/apify_oauth.app.mjs(1 hunks)
- components/booking_experts/booking_experts.app.mjs(1 hunks)
- components/bridge_interactive_platform/bridge_interactive_platform.app.mjs(1 hunks)
- components/hubspot/common/constants.mjs(2 hunks)
- components/hubspot/package.json(2 hunks)
✅ Files skipped from review due to trivial changes (3)
- components/apify_oauth/apify_oauth.app.mjs
- components/booking_experts/booking_experts.app.mjs
- components/bridge_interactive_platform/bridge_interactive_platform.app.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
- components/hubspot/package.json
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Verify TypeScript components
- GitHub Check: Publish TypeScript components
- GitHub Check: pnpm publish
- GitHub Check: Lint Code Base
🔇 Additional comments (1)
components/hubspot/common/constants.mjs (1)
23-23: Confirm Automation API version usageThe addition of
API_PATH.AUTOMATIONV4looks great. However, I see that several workflow‐related methods are still pointing at the legacy v2 path. Please verify whether these should be migrated to the v4 endpoint or intentionally remain on v2 for backward compatibility.• components/hubspot/hubspot.app.mjs (lines 1189–1193):
listWorkflows(opts = {}) { return this.makeRequest({ api: API_PATH.AUTOMATION, // v2 endpoint: "/workflows", …opts, }) }• components/hubspot/hubspot.app.mjs (lines 1199–1202):
return this.makeRequest({ api: API_PATH.AUTOMATION, // v2 endpoint: `/workflows/${workflowId}/enrollments/contacts/${contactEmail}`, method: "POST", })• components/hubspot/hubspot.app.mjs (lines 1481–1483):
return this.makeRequest({ api: API_PATH.AUTOMATIONV4, // v4 (flows) endpoint: "/flows", …opts, })Please confirm which workflow actions should use
API_PATH.AUTOMATIONV4to avoid mixed‐version behavior.
| /approve | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @luancazarine lgtm! Ready for QA!
| /approve | 
| Hi @luancazarine could you please rebase so I can approve! | 
Resolves #18029
Summary by CodeRabbit
New Features
Improvements
Chores