Skip to content

Add HubSpot conversation threads with internal notes support #17607

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import hubspot from "../../hubspot.app.mjs";

export default {
key: "hubspot-add-conversation-comment",
name: "Add Conversation Comment (Internal Note)",
description: "Add an internal comment to a HubSpot conversation thread. Internal comments are only visible to team members. [See the documentation](https://developers.hubspot.com/docs/api/conversations/threads)",
version: "0.0.1",
type: "action",
props: {
hubspot,
threadId: {
propDefinition: [
hubspot,
"threadId",
],
},
text: {
type: "string",
label: "Comment Text",
description: "The plain text content of the internal comment",
},
richText: {
type: "string",
label: "Rich Text",
description: "The rich text/HTML content of the internal comment",
optional: true,
},
},
async run({ $ }) {
if (!this.text?.trim()) {
throw new Error("Comment text cannot be empty");
}

const response = await this.hubspot.addConversationComment({
threadId: this.threadId,
data: {
text: this.text,
richText: this.richText || this.text,
type: "COMMENT",
},
$,
});

$.export("$summary", `Successfully added internal comment to conversation thread ${this.threadId}`);
return response;
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import hubspot from "../../hubspot.app.mjs";

export default {
key: "hubspot-send-conversation-message",
name: "Send Conversation Message",
description: "Send a message to a HubSpot conversation thread. [See the documentation](https://developers.hubspot.com/docs/api/conversations/threads)",
version: "0.0.1",
type: "action",
props: {
hubspot,
threadId: {
propDefinition: [
hubspot,
"threadId",
],
},
text: {
type: "string",
label: "Message Text",
description: "The plain text content of the message",
},
richText: {
type: "string",
label: "Rich Text",
description: "The rich text/HTML content of the message",
optional: true,
},
direction: {
type: "string",
label: "Direction",
description: "The direction of the message",
options: [
{
label: "Outgoing",
value: "OUTGOING",
},
{
label: "Incoming",
value: "INCOMING",
},
],
default: "OUTGOING",
},
},
async run({ $ }) {
if (!this.text?.trim()) {
throw new Error("Message text cannot be empty");
}

const response = await this.hubspot.sendConversationMessage({
threadId: this.threadId,
data: {
text: this.text,
richText: this.richText || this.text,
direction: this.direction,
type: "MESSAGE",
},
$,
});

$.export("$summary", `Successfully sent message to conversation thread ${this.threadId}`);
return response;
},
};
63 changes: 63 additions & 0 deletions components/hubspot/hubspot.app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ export default {
: [];
},
},
threadId: {
type: "string",
label: "Thread ID",
description: "HubSpot conversation thread ID",
},
objectIds: {
type: "string[]",
label: "Object",
Expand Down Expand Up @@ -1142,5 +1147,63 @@ export default {
...opts,
});
},
/**
* Get conversation thread details
* @param {string} threadId - The ID of the conversation thread
* @param {object} opts - Additional options to pass to the request
* @returns {Promise<object>} The conversation thread object
*/
getConversationThread({
threadId, ...opts
}) {
return this.makeRequest({
endpoint: `/conversations/v3/conversations/threads/${threadId}`,
...opts,
});
},
/**
* Get messages from a conversation thread
* @param {string} threadId - The ID of the conversation thread
* @param {object} opts - Additional options to pass to the request
* @returns {Promise<object>} The messages in the conversation thread
*/
getConversationMessages({
threadId, ...opts
}) {
return this.makeRequest({
endpoint: `/conversations/v3/conversations/threads/${threadId}/messages`,
...opts,
});
},
/**
* Send a message to a conversation thread
* @param {string} threadId - The ID of the conversation thread
* @param {object} opts - Message data and request options
* @returns {Promise<object>} The sent message object
*/
sendConversationMessage({
threadId, ...opts
}) {
return this.makeRequest({
endpoint: `/conversations/v3/conversations/threads/${threadId}/messages`,
method: "POST",
...opts,
});
},
/**
* Add an internal comment to a conversation thread
* @param {string} threadId - The ID of the conversation thread
* @param {object} opts - Comment data and request options
* @returns {Promise<object>} The added comment object
*/
addConversationComment({
threadId, ...opts
}) {
return this.makeRequest({
endpoint: `/conversations/v3/conversations/threads/${threadId}/messages`,
method: "POST",
...opts,
});
},
Comment on lines +1150 to +1207
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Critical issue: Missing API parameter in conversation methods

All the new conversation methods are missing the api parameter when calling makeRequest. Looking at the existing pattern in this file, other methods specify an API path like API_PATH.CRMV3.

Without the api parameter, the makeRequest method will construct URLs like ${BASE_URL}undefined${endpoint}, which is incorrect.

You need to add the appropriate API path parameter to all four conversation methods. For example:

getConversationThread({
  threadId, ...opts
}) {
  return this.makeRequest({
+   api: API_PATH.CONVERSATIONS, // or appropriate API path constant
    endpoint: `/conversations/v3/conversations/threads/${threadId}`,
    ...opts,
  });
},

Run this script to verify the API path constant that should be used:


🏁 Script executed:

#!/bin/bash
# Check for existing API path constants in the constants file
rg "API_PATH" --type js -A 5 -B 5

# Look for any conversation-related API constants
rg "conversation" --type js -i

Length of output: 64


To identify the correct API path constant and how it’s used in this file, let’s inspect imports, existing makeRequest calls, and any conversation-related constants in hubspot.app.mjs:


🏁 Script executed:

#!/bin/bash
# 1. See if API_PATH is imported
rg "import .*API_PATH" -n components/hubspot/hubspot.app.mjs

# 2. Find any references to API_PATH in the file
rg "API_PATH" -n components/hubspot/hubspot.app.mjs

# 3. Show existing makeRequest calls (to see how api: is used elsewhere)
rg "makeRequest" -n -A 3 -B 3 components/hubspot/hubspot.app.mjs

# 4. Check for any conversation-related constants in this file
rg "CONVERSATION" -n components/hubspot/hubspot.app.mjs

Length of output: 14117


To confirm which API paths are defined, let’s locate and inspect the file that defines API_PATH:


🏁 Script executed:

#!/bin/bash
# Find where API_PATH is defined
rg "API_PATH\s*=" -n .

# Display the first definition file to view available constants
FILE=$(rg -l "API_PATH\s*=" . | head -n1)
sed -n '1,200p' "$FILE"

Length of output: 5109


Let’s pull in the HubSpot API_PATH definitions to see which constant to use:


🏁 Script executed:

#!/bin/bash
# Show the API_PATH block in the HubSpot constants file
rg "const API_PATH" -n -A50 components/hubspot/common/constants.mjs

Length of output: 1354


Add api parameter and new CONVERSATIONS path constant for conversation methods

The four conversation methods currently call

this.makeRequest({
  endpoint: `/conversations/v3/conversations/threads/${threadId}`,
  …opts,
});

without an api field, so makeRequest will prepend undefined to the URL. There is no existing API_PATH.CONVERSATIONS constant—so:

• In components/hubspot/common/constants.mjs, add to the API_PATH object:

CONVERSATIONS: "/conversations/v3",

• In components/hubspot/hubspot.app.mjs, update each method to include api: API_PATH.CONVERSATIONS and remove the duplicated version prefix in the endpoint. For example:

 getConversationThread({ threadId, ...opts }) {
-  return this.makeRequest({
-    endpoint: `/conversations/v3/conversations/threads/${threadId}`,
-    ...opts,
-  });
+  return this.makeRequest({
+    api: API_PATH.CONVERSATIONS,
+    endpoint: `/conversations/threads/${threadId}`,
+    ...opts,
+  });
},

Repeat for getConversationMessages, sendConversationMessage, and addConversationComment.
This ensures URLs are constructed as:
<BASE_URL>/conversations/v3/conversations/threads/{threadId}.

🤖 Prompt for AI Agents
In components/hubspot/hubspot.app.mjs lines 1150 to 1207, the conversation
methods lack the 'api' parameter, causing makeRequest to prepend 'undefined' to
URLs, and the conversation path prefix is duplicated in endpoints. Fix this by
adding a new constant CONVERSATIONS: "/conversations/v3" to the API_PATH object
in components/hubspot/common/constants.mjs, then update each conversation method
to include api: API_PATH.CONVERSATIONS and remove the '/conversations/v3' prefix
from the endpoint strings, ensuring URLs are correctly constructed as
<BASE_URL>/conversations/v3/conversations/threads/{threadId}.

},
};
2 changes: 1 addition & 1 deletion components/hubspot/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pipedream/hubspot",
"version": "1.2.4",
"version": "1.2.5",
"description": "Pipedream Hubspot Components",
"main": "hubspot.app.mjs",
"keywords": [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import common from "../common/common.mjs";

export default {
...common,
key: "hubspot-new-conversation-comment",
name: "New Conversation Comment (Internal Note)",
description: "Emit new event when a new internal comment is added to a HubSpot conversation thread. [See the documentation](https://developers.hubspot.com/docs/api/conversations/threads)",
version: "0.0.1",
type: "source",
dedupe: "unique",
props: {
...common.props,
threadId: {
propDefinition: [
common.props.hubspot,
"threadId",
],
description: "Filter comments from a specific conversation thread",
optional: true,
},
},
methods: {
...common.methods,
getTs(comment) {
return Date.parse(comment.createdAt);
},
generateMeta(comment) {
return {
id: comment.id,
summary: `New Internal Comment: ${comment.text || comment.id}`,
ts: this.getTs(comment),
};
},
isRelevant(comment, createdAfter) {
const isAfterTimestamp = this.getTs(comment) > createdAfter;
const matchesThread = !this.threadId || comment.threadId === this.threadId;
const isComment = comment.type === "COMMENT";

return isAfterTimestamp && matchesThread && isComment;
},
async getParams() {
return {
params: {
limit: 100,
},
};
},
async processResults(after, params) {
const createdAfter = after || this.getLastCreatedAt();

if (this.threadId) {
// If specific thread is provided, get messages from that thread
const messages = await this.hubspot.getConversationMessages({
threadId: this.threadId,
...params,
});

const comments = messages.results?.filter(msg =>
this.isRelevant(msg, createdAfter)
) || [];

this.processEvents(comments);
} else {
// Note: HubSpot Conversations API doesn't provide a direct way to list all threads
// This would require HubSpot webhooks or a different approach
console.log("Thread-specific monitoring recommended - provide threadId prop for best results");
}
},
Comment on lines +48 to +68
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Incomplete implementation for non-thread-specific monitoring.

The processResults method handles the thread-specific case well, but the else branch (lines 63-67) only logs a message without actually processing any events. This means the component won't emit any events when no threadId is provided, which may not be the intended behavior.

Consider implementing a fallback mechanism or providing clearer documentation about the threadId requirement:

} else {
-  // Note: HubSpot Conversations API doesn't provide a direct way to list all threads
-  // This would require HubSpot webhooks or a different approach
-  console.log("Thread-specific monitoring recommended - provide threadId prop for best results");
+  // Note: HubSpot Conversations API doesn't provide a direct way to list all threads
+  // This would require HubSpot webhooks or a different approach
+  throw new Error("threadId is required for conversation comment monitoring due to HubSpot API limitations");
}

Alternatively, if the component is designed to work only with threadId, consider making it a required prop instead of optional.

Let me verify how other similar HubSpot components handle this scenario:


🏁 Script executed:

#!/bin/bash
# Description: Check how other HubSpot conversation components handle optional threadId
# Expected: Find similar patterns or required threadId usage

# Search for similar threadId usage patterns in HubSpot components
rg -A 10 -B 5 "threadId.*optional" components/hubspot/

# Search for other conversation-related components
fd -e mjs -p "conversation" components/hubspot/

Length of output: 430


I’ll gather how the sibling source handles threadId and its fallback behavior.


🏁 Script executed:

#!/bin/bash
# 1. Inspect threadId prop definition in new-conversation-comment
rg -A 5 -B 5 "threadId" components/hubspot/sources/new-conversation-comment/new-conversation-comment.mjs

# 2. Show processResults in the sibling new-conversation-message source
rg -A 20 -B 5 "async processResults" components/hubspot/sources/new-conversation-message/new-conversation-message.mjs

Length of output: 2651


Consistent threadId requirement across HubSpot conversation sources

Both new-conversation-comment and its sibling new-conversation-message log a recommendation and emit no events when threadId is omitted. Because the HubSpot Conversations API doesn’t support listing all threads, this behavior is by design.

Please choose one of the following to make the contract clearer:

• Make threadId a required prop in common.props.hubspot.threadId (optional: false) so consumers see a compile-time error if they omit it.
• Enhance the component documentation (in the README or prop descriptions) to explicitly state that threadId is required and that omitting it results in no events.

Locations to update:

  • components/hubspot/sources/new-conversation-comment/new-conversation-comment.mjs (processResults else-branch)
  • components/hubspot/sources/new-conversation-message/new-conversation-message.mjs (processResults else-branch)
  • common prop definition: common.props.hubspot.threadId
🤖 Prompt for AI Agents
In
components/hubspot/sources/new-conversation-comment/new-conversation-comment.mjs
lines 48-68 and similarly in new-conversation-message.mjs, the processResults
method logs a message and emits no events if threadId is omitted, which is by
design due to API limitations. To clarify this contract, update the common prop
definition common.props.hubspot.threadId to set optional: false, making threadId
a required prop and causing compile-time errors if omitted. Alternatively,
enhance the component documentation to explicitly state that threadId is
required and omitting it results in no events. Apply these changes consistently
across the mentioned files and the common prop definition.

},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import common from "../common/common.mjs";

export default {
...common,
key: "hubspot-new-conversation-message",
name: "New Conversation Message",
description: "Emit new event when a new message is added to a HubSpot conversation thread. [See the documentation](https://developers.hubspot.com/docs/api/conversations/threads)",
version: "0.0.1",
type: "source",
dedupe: "unique",
props: {
...common.props,
threadId: {
propDefinition: [
common.props.hubspot,
"threadId",
],
description: "Filter messages from a specific conversation thread",
optional: true,
},
messageType: {
type: "string",
label: "Message Type",
description: "Filter by message type",
options: [
{
label: "All Messages",
value: "",
},
{
label: "Regular Messages",
value: "MESSAGE",
},
{
label: "Internal Comments",
value: "COMMENT",
},
],
default: "",
optional: true,
},
},
methods: {
...common.methods,
getTs(message) {
return Date.parse(message.createdAt);
},
generateMeta(message) {
const messageType = message.type === "COMMENT" ? "Internal Comment" : "Message";
return {
id: message.id,
summary: `New ${messageType}: ${message.text || message.id}`,
ts: this.getTs(message),
};
},
isRelevant(message, createdAfter) {
const isAfterTimestamp = this.getTs(message) > createdAfter;
const matchesThread = !this.threadId || message.threadId === this.threadId;
const matchesType = !this.messageType || message.type === this.messageType;

return isAfterTimestamp && matchesThread && matchesType;
},
async getParams() {
return {
params: {
limit: 100,
},
};
},
async processResults(after, params) {
const createdAfter = after || this.getLastCreatedAt();

if (this.threadId) {
// If specific thread is provided, get messages from that thread
const messages = await this.hubspot.getConversationMessages({
threadId: this.threadId,
...params,
});

const relevantMessages = messages.results?.filter(msg =>
this.isRelevant(msg, createdAfter)
) || [];

this.processEvents(relevantMessages);
} else {
// Note: HubSpot Conversations API doesn't provide a direct way to list all threads
// This would require HubSpot webhooks or a different approach
console.log("Thread-specific monitoring recommended - provide threadId prop for best results");
}
},
},
};