Skip to content

Copying line items in custom code

n this comprehensive guide, we delve into the intricacies of implementing a custom-coded action within a HubSpot workflow, specifically tailored for the purpose of cloning line items from one deal to another. This functionality proves invaluable in scenarios such as deal renewals, where duplicating line items from the original deal to the renewal deal is required. By automating this process, we significantly streamline operations, enhancing efficiency and accuracy.

Objective of this workflow

The primary goal of this workflow is to automate the cloning of line items from a "parent" deal to a "renewal" deal. This ensures that once-off line items are optionally included based on your preference, providing flexibility to suit various business models.

Prerequisites

Before we dive into the code, it's important to note that we've previously discussed how to connect to APIs, including HubSpot's API, read data, manipulate it, and write it back to the CRM. This guide assumes you are familiar with these concepts. We encourage you to go through our HubSpot Kickbox integration explanation and HubSpot Clone Deal workflow explanation if you have not already done so. 

Breakdown of the code

The workflow is constructed with JavaScript, utilizing the HubSpot API Client. Each step of the process is carefully outlined below, accompanied by relevant code snippets to illustrate the implementation details.

Step 1: Initialization and Setup

Initially, we define whether to include once-off payments by toggling a boolean variable. We then import the necessary HubSpot API client library.

// Exclude once-off payments ("recurringbillingfrequency": null):
const OnceOffPayments_included = false;

// Import required libraries
const hubspot = require('@hubspot/api-client');

Step 2: Main Function Definition

The main function is declared as an asynchronous operation to handle API calls effectively. Inside this function, a new HubSpot API Client is instantiated using an access token stored in an environment variable.

exports.main = async (event, callback) => {
    // Create a new HubSpot API Client
    const hubspotClient = new hubspot.Client({
        accessToken: process.env.PrivateAppDealActions
    });

Step 3a: Making Input Fields accessable

We only have 2 inputfields:
The record ID itself, which is the currently enrolled object. So that means the cloned deal that does not have the line items associated yet.

And the parent_id. This is a custom property and in the workflow that clones the deal we can simply set that. this can be set both in coded workflows as well in no-code workflows that clone the deals.

inputFields lineitems

Step 3b: Retrieving Input Fields

We fetch the hs_object_id and parent_id from the event's input fields. These identifiers are crucial for locating the current deal and its parent from which line items will be cloned.

  //Retrieving the included properties
  const hs_object_id = event.inputFields['hs_object_id'];  
  const parent_id = event.inputFields['parent_id'];

Step 4: Fetching Associations with a Helper Function

A custom helper function, fetchAssociations, simplifies the retrieval of line item associations for the parent deal. This method abstracts the complexity of making paginated API calls and error handling.

    async function fetchAssociations(fromObjectType, fromObjectId, toObjectType) {
        try {
            const response = await hubspotClient.crm.associations.v4.basicApi.getPage(fromObjectType, fromObjectId, toObjectType, undefined, 100);
            return response.results.map(({ toObjectId }) => String(toObjectId));
        } catch (error) {
            console.error(`Failed to fetch ${toObjectType} associations for ${fromObjectType} ${fromObjectId}:`, error.message);
            return [];
        }
    }

Step 5: Cloning and Association Process

The fetchAssociations function is utilized to gather line item IDs associated with the parent deal. Subsequently, a structured input is prepared for each line item, including its properties and direct associations to the renewal deal, facilitating the batch creation process.

    const associatedLineItemsParent = await fetchAssociations('deals', parent_id, 'line_items');

    const inputs = await Promise.all(associatedLineItemsParent.map(async (lineItemId) => {
        const lineItemResponse = await hubspotClient.crm.lineItems.basicApi.getById(lineItemId, [
            'name',
            'quantity',
            'price',
            // Add other necessary properties here
        ]);
        
        const properties = lineItemResponse.properties;
        ['createdate', 'hs_lastmodifieddate', 'hs_object_id'].forEach(prop => delete properties[prop]);

        const associations = OnceOffPayments_included || properties.recurringbillingfrequency !== null ? [{
            to: { id: hs_object_id },
            types: [
                {
                    associationCategory: "HUBSPOT_DEFINED",
                    associationTypeId: 20,
                },
            ],
        }] : [];

        return { 
            properties,
            associations
        };
    }));

Step 6: Batch Creation of Cloned Line Items

The prepared inputs are used in a batch API call to create new line items and associate them with the renewal deal in a single operation. This method enhances efficiency by minimizing the number of required API calls.

    try {
        const apiResponse = await hubspotClient.crm.lineItems.batchApi.create({ inputs });
        console.log(JSON.stringify(apiResponse, null, 2));

        callback({
            outputFields: {
                // Output fields here if necessary
            }
        });
    } catch (e) {
        console.error('Error:', e.message || 'An error occurred');
        callback({ error: e.message });
    }
};

Conclusion

The script outlined above provides a streamlined and efficient solution for cloning line items from one deal to another within HubSpot. By leveraging JavaScript alongside HubSpot's API Client, we automate a previously manual and error-prone process, enhancing operational efficiency and data accuracy. This workflow is adaptable, catering to different business needs by allowing the inclusion or exclusion of once-off payments. Through automation, we free up valuable time and resources, enabling focus on strategic business activities that drive growth and success.

Final code result

Be mindful that in HubSpot ever coded workflow has settings for the language and the SDK version. This workflow is written in NodeJS 20 and SDK v11.

Here is the completed code:

// Exclude once-off payments ("recurringbillingfrequency": null):
const OnceOffPayments_included = false;

// Import required libraries
const hubspot = require('@hubspot/api-client');

exports.main = async (event, callback) => {
    // Create a new HubSpot API Client
    const hubspotClient = new hubspot.Client({
        accessToken: process.env.PrivateAppDealActions
    });

    // Retrieving the included input fields
    const hs_object_id = event.inputFields['hs_object_id'];
    const parent_id = event.inputFields['parent_id'];

    // Helper function to fetch associations
    async function fetchAssociations(fromObjectType, fromObjectId, toObjectType) {
        try {
            const response = await hubspotClient.crm.associations.v4.basicApi.getPage(fromObjectType, fromObjectId, toObjectType, undefined, 100);
            return response.results.map(({ toObjectId }) => String(toObjectId));
        } catch (error) {
            console.error(`Failed to fetch ${toObjectType} associations for ${fromObjectType} ${fromObjectId}:`, error.message);
            return [];
        }
    }

    // Use the fetchAssociations function to retrieve line item associations for the parent deal
    const associatedLineItemsParent = await fetchAssociations('deals', parent_id, 'line_items');

    // Prepare inputs for cloning the filtered line items including associations directly in the batch creation
    const inputs = await Promise.all(associatedLineItemsParent.map(async (lineItemId) => {
        const lineItemResponse = await hubspotClient.crm.lineItems.basicApi.getById(lineItemId, [
            'name',
            'quantity',
            'price',
            // Add other necessary properties here
        ]);
        
        const properties = lineItemResponse.properties;
        // Exclude properties not needed for the clone or that should not be copied
        ['createdate', 'hs_lastmodifieddate', 'hs_object_id'].forEach(prop => delete properties[prop]);

        // Directly include the deal association in the input for the new line item
        const associations = OnceOffPayments_included || properties.recurringbillingfrequency !== null ? [{
            to: { id: hs_object_id },
            types: [
                {
                    associationCategory: "HUBSPOT_DEFINED",
                    associationTypeId: 20, // deals -> lineitems = 19, lineitems -> deals = 20
                },
            ],
        }] : [];

        return { 
            properties,
            associations
        };
    }));

    try {
        // Execute the batch create call including associations
        const apiResponse = await hubspotClient.crm.lineItems.batchApi.create({ inputs });
        console.log(JSON.stringify(apiResponse, null, 2)); // Log the API response for verification

        // Success callback
        callback({
            outputFields: {
                // Output fields here if necessary
            }
        });
    } catch (e) {
        console.error('Error:', e.message || 'An error occurred');
        callback({ error: e.message });
    }
};