Skip to content

Conversation

@luancazarine
Copy link
Collaborator

@luancazarine luancazarine commented Aug 22, 2025

Resolves #18029

Summary by CodeRabbit

  • New Features

    • Added many HubSpot actions: create/update pages, landing pages, forms, marketing emails; clone site pages and emails; create contact workflows; update form fields; create leads with associations.
  • Improvements

    • Global API rate limiting for HubSpot requests.
    • New UI pickers for forms, pages, landing pages, templates, campaigns, and marketing emails.
    • Added language options and payload-cleaning utility for content APIs.
  • Chores

    • Package and metadata version bumps; numerous import reorderings.

- 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.
@luancazarine luancazarine linked an issue Aug 22, 2025 that may be closed by this pull request
@vercel
Copy link

vercel bot commented Aug 22, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
pipedream-docs Ignored Ignored Aug 28, 2025 7:56pm
pipedream-docs-redirect-do-not-edit Ignored Ignored Aug 28, 2025 7:56pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 22, 2025

Walkthrough

Adds 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

Cohort / File(s) Summary
New CMS/Marketing actions
components/hubspot/actions/create-form/create-form.mjs, components/hubspot/actions/update-fields-on-the-form/update-fields-on-the-form.mjs, components/hubspot/actions/create-page/create-page.mjs, components/hubspot/actions/update-page/update-page.mjs, components/hubspot/actions/clone-site-page/clone-site-page.mjs, components/hubspot/actions/create-landing-page/create-landing-page.mjs, components/hubspot/actions/update-landing-page/update-landing-page.mjs, components/hubspot/actions/create-email/create-email.mjs, components/hubspot/actions/clone-email/clone-email.mjs, components/hubspot/actions/create-contact-workflow/create-contact-workflow.mjs, components/hubspot/actions/update-fields-on-the-form/update-fields-on-the-form.mjs
Adds actions to create/clone/update forms, pages, landing pages, emails, and contact workflows; actions build payloads (use parseObject/cleanObject), call new hubspot.app methods, validate inputs, export summaries.
App extensions & propDefinitions
components/hubspot/hubspot.app.mjs
Adds Bottleneck-based rate limiting (axiosRateLimiter), many new methods (create/update/list forms, pages, landing pages, emails, workflows, cloneSitePage, listLandingFolders, template helpers), and new propDefinitions (formId, pageId, landingPageId, templatePath, landingFolderId, campaignId, emailId) with async options.
Shared props & utils
components/hubspot/actions/common/common-page-prop.mjs, components/hubspot/common/utils.mjs, components/hubspot/common/constants.mjs
Adds commonPageProp schema for page fields, introduces cleanObject(obj) utility, adds LANGUAGE_OPTIONS and API_PATH.AUTOMATIONV4, and reorders/extends exported constants.
CRM tweak: lead associations
components/hubspot/actions/create-lead/create-lead.mjs
When creating a lead, attaches an associations payload linking the lead to contactId (HUBSPOT_DEFINED, associationTypeId 578).
Version bumps & import reorders
components/hubspot/actions/**, components/hubspot/sources/** (many files; see diff)
Numerous action/source version increments, minor import reorderings, added ConfigurationError imports in some files; no functional changes in those files.
Package metadata
components/hubspot/package.json
Bumps package version 1.6.01.6.1 and updates @pipedream/platform dependency ^3.0.0^3.1.0.

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
Loading
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}"
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Assessment against linked issues

Objective Addressed Explanation
Duplicate/create and edit forms [#18029]
Clone/create and edit pages (CMS) [#18029]
Clone/create emails (marketing) [#18029]
Clone/create workflows (automation) [#18029] Only createContactWorkflow added; no clone workflow action implemented.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Add associations linking new lead to contact (components/hubspot/actions/create-lead/create-lead.mjs) CRM association behavior change is not part of CMS/Marketing objectives in #18029.
Introduce Bottleneck rate limiter and global request throttling (components/hubspot/hubspot.app.mjs) Global rate-limiting is an infrastructure/throughput change outside the CMS/Marketing feature request.
Package dependency bump (components/hubspot/package.json) Dependency/version update is packaging infra and not required by the feature objectives.

Possibly related PRs

Suggested reviewers

  • michelle0927

Poem

I hop through fields and pages bright,
I clone an email in the night,
I tidy props and clean the heap,
I throttle calls so APIs sleep.
A rabbit cheers — commit takes flight! 🥕

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 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.

📥 Commits

Reviewing files that changed from the base of the PR and between e1dfca3 and 52c10b1.

📒 Files selected for processing (1)
  • components/hubspot/actions/get-associated-emails/get-associated-emails.mjs (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • components/hubspot/actions/get-associated-emails/get-associated-emails.mjs
⏰ 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)
  • GitHub Check: Verify TypeScript components
  • GitHub Check: Publish TypeScript components
  • GitHub Check: Lint Code Base
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 18029-actionhubspot-cms-and-marketing-api

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

…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.
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 when after is undefined and coerce it to a string

On first runs, after will be undefined, 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 getParams to:

   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 avoid value: undefined
  • Uses String(after) to satisfy the API’s requirement for string‐typed filter values
components/hubspot/actions/list-marketing-emails/list-marketing-emails.mjs (1)

81-116: Critical bug: results shadowing 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 same results you’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?.after and only set params.after when 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 destructured results from the API (Line 91).
  • Inside the loop you push into the API’s results while iterating it, not the accumulator (Lines 99-101). This both duplicates items and leaves the outer results empty, 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: Align sort.options with HubSpot’s CMS Blog Posts API property names

The sort parameter itself is correct—HubSpot expects a sort query 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 createdBycreatedById
  • Replace updatedByupdatedById

Additionally, if you wish to support sorting by publish date (a common use case), consider adding publishDate to the options.

Please update the snippet in components/hubspot/actions/list-blog-posts/list-blog-posts.mjs accordingly:

     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 results array 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 outer results stays 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 id

If find returns undefined, accessing file.id throws before your explicit not-found check. Validate file first.

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 included

Two issues:

  • Line 28 collects email without filtering falsy values, so undefined can be sent to the IN filter.
  • Line 42 sets updateEmails = results?.map(...), which becomes undefined when results is falsy, leading to updateEmails.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 inputs

Current flow risks:

  • Contacts with an id can 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 id and without email are silently processed, but HubSpot can’t create a contact without at least an email.

Refactor:

  • Validate each contact has either id (update) or email (create/update).
  • 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 endpoints

The batchCreateContacts and batchUpdateContacts methods in hubspot.app.mjs do not enforce the 100-item limit; they simply pass through your opts as-is. To avoid request failures when insertProperties or updateProperties exceeds 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:

  1. Splits the full inputs array into subarrays of at most 100 entries.
  2. Calls this.hubspot.batchCreateContacts (or batchUpdateContacts) for each chunk.
  3. 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 results

Inside the loop:

  • You destructure results from the API response, which shadows the outer results accumulator.
  • You then for ... of results and push to results inside 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 reports 0.

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 inverted

For a custom range, start should be GTE startDate and end should be LTE endDate. 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 ...customObjects will 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 for hubspot.listTasks

Verified that listTasks in hubspot.app.mjs invokes this.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 results to [] to keep ...customObjects safe.

-      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: Bind listNotes to the HubSpot client context

The listNotes implementation invokes this.makeRequest, so binding it to the component’s this will break its internal this reference. Update the call in components/hubspot/sources/new-note/new-note.mjs accordingly:

• 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 filled

If 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: Derive associationCategory dynamically instead of hard-coding HUBSPOT_DEFINED

The 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)
    The associationType propDefinition returns only an integer (typeId) as the value—and no category field—so downstream code can’t know the correct category.
  • components/hubspot/actions/create-meeting/create-meeting.mjs (lines 100–105)
    The types array is still hard-coded to HUBSPOT_DEFINED.

You should refactor one of two ways:

  1. Extend the prop to return both id and category:
    In common.props.hubspot.associationType.options, map each type to

    return associationTypes.map((t) => ({
      label: t.label,
    - value: t.typeId,
    + value: { id: t.typeId, category: t.category },
    }));

    Then update your create-meeting action to unpack that object.

  2. 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.mjs to 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_DEFINED for 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 (not this.*).

results.find(...) may return undefined, causing association.category to throw. Also, prefer the passed associationType arg over this.associationType inside 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 to error.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 Logic

I 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.mjs or 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 catch block, 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 options

If a name listed in schema.searchableProperties isn’t present in schema.properties, propData will be undefined, 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 of searchProperty before calling the API

If searchProperty is undefined, the current message is misleading. Also, guard against schemas that don’t expose searchableProperties.

-    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);
+      });
+    }

luancazarine and others added 5 commits August 22, 2025 17:34
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>
jcortes
jcortes previously approved these changes Aug 22, 2025
Copy link
Collaborator

@jcortes jcortes left a 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!

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 of LANGUAGE_OPTIONS is 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
fi

Once 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 2a4398a and e1dfca3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is 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 usage

The addition of API_PATH.AUTOMATIONV4 looks 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.AUTOMATIONV4 to avoid mixed‐version behavior.

@luancazarine
Copy link
Collaborator Author

/approve

jcortes
jcortes previously approved these changes Aug 25, 2025
Copy link
Collaborator

@jcortes jcortes left a 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!

@luancazarine
Copy link
Collaborator Author

/approve

@jcortes
Copy link
Collaborator

jcortes commented Aug 27, 2025

Hi @luancazarine could you please rebase so I can approve!

@luancazarine luancazarine merged commit 24f033c into master Aug 28, 2025
10 checks passed
@luancazarine luancazarine deleted the 18029-actionhubspot-cms-and-marketing-api branch August 28, 2025 19:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ACTION]Hubspot CMS and Marketing API

3 participants