Skip to content

Cloning a deal

In this article, we'll break down a custom-coded workflow in HubSpot. This workflow is designed to clone a deal when it reaches a certain pipeline stage. By the end of this guide, you should have a good understanding of how this code functions and be able to implement it in your own HubSpot environment. Please do note that you can just as easily create a new workflow in HubSpot and in the same non-coded action copy over most fields and the contact- and company associations. However, this is meant to be educational and much more flexible. We will build upon this workflow in the next workflow that copies over the line items as well.

Introduction

This custom-coded action uses the HubSpot API and JavaScript to create a clone of a deal when it enters a specific stage in the pipeline. The cloned deal will retain most of the properties of the original deal but will also have some of its properties modified such as the deal name and renewal date. The new deal will also be associated with the same companies and contacts as the original deal.

The code primarily does four things:

  1. Retrieves and modifies certain properties of the original deal.
  2. Creates a new deal with these properties.
  3. Associates the new deal with the same companies as the original deal.
  4. Associates the new deal with the same contacts as the original deal.

Code Breakdown

Setting up the Environment

The code starts by setting up the required settings and importing the necessary libraries.

const pipeline = 22679326; // new pipeline
const dealstage = 53954819; // new dealstage
Deal and pipeline ID

Creating a New HubSpot Client

Here we will start by importing the HubSpot API's and setting up the client. We will a secret called 'PrivateAppDealActions'. Except for the secret name this is identical to the way we set it up in the KickBox custom coded integration. This client is used throughout the rest of the code to interact with the HubSpot API.

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

const hubspotClient = new hubspot.Client({
  accessToken: process.env.PrivateAppDealActions
});

Retrieving and Modifying Deal Properties

We will use some of the original deal properties to create the newly cloned deal. According to HubSpot's best practices we will pass these along as inputfields. Please note that we can pass a maximum of 50 properties this way. If we would need more, we can easily call the deal object through the API and work with all the properties we need. In this workflow we use these properties. Please note that your portal may not use or may not have all these properties:

Clone lineitems: Properties included

Now we need to load these in the code. We could do that where they are required, resulting in cleaner code. But for readability purposes we will simply call the whole list:

  //Retrieving the included properties 
  const autorenew = Boolean(event.inputFields['AutoRenew']);  // custom property!!!
  const hubspot_owner_id = Number(event.inputFields['hubspot_owner_id']);
  const business_unit = event.inputFields['business_unit'];    // custom property!!! No need for this if you don't use business units on a deal level.
  const deal_currency_code = event.inputFields['deal_currency_code'];
  const description = event.inputFields['description'];
  const dealname = event.inputFields['dealname'];
  const hs_analytics_source = event.inputFields['hs_analytics_source'];
  const renewal_date = Number(event.inputFields['renewal_date']);
  const autorenew_duration = Number(event.inputFields['autorenew_duration']);  // custom property!!!
  const hs_mrr = Number(event.inputFields['hs_mrr']);
  const success_owner_id = event.inputFields['success_owner_id'];  // custom property!!!
  const contract_owner_id = event.inputFields['contract_owner_id'];  // custom property!!!
  const hs_object_id = event.inputFields['hs_object_id'];

In this part, we set the pipeline and dealstage variables, which represent the new pipeline and dealstage for the cloned deal.

We can retrieve these ID's in the HubSpot portal by going to the settings, and under Data Management selecting Objects -> Deals.

Then select the Pipelines tab and hover click on the </> for the pipeline ID.
To get the Dealstage ID hover over the stage name first to make the </> visible, and then click that.

Amending properties for the new deal

The main benefit of using custom code over the no-code action of creating a deal object and copying over the property values of the original deal is that we can amend them using any form of logic we can come up with. Say we want to calculate the new renewal date by adding the renewal duration and the end date of the previous deal. Simple enough.
And calculate the new deal amount based on the MRR times the auto-renew duration? Just one line of code.
Same with changing the name. We want all renewal deals to end with "- Renewal", but simply adding that again and again is just plain wrong. So we will split the name before " - " and put " - Renewal" after that. 
These are just simple examples. But you can include any logics you want. Here is what we included for the mentioned amendments:

  // Amend some of the properties
  const new_renewal_date = new Date(renewal_date)
	new_renewal_date.setMonth(new_renewal_date.getMonth() + autorenew_duration)

  // calculate new ammount based on MRR
  const new_amount = hs_mrr * autorenew_duration; // could do with a bit more logics.

  // remove part of name after ' - ' and add renewal.
  const new_dealname =  dealname.split(" - ", 1) + " - Renewal";

Setting the properties for the new deal

We will now create an object called 'properties' and include all the properties we want to pass along. Please note that HubSpot does also set some of the deal properties like 'createdate' itself and there is no need to pass that along. If you are unsure of the exact name of a property: go into the settings, click on 'properties', select 'deal properties' and click on the name of the property. That will include a similar '</>' snippet that if clicked upon reveals the name as we should use it below. 

  //Set properties for the renewal deal        
  const properties = {
    "amount": new_amount,
    "autorenew": autorenew,
    "business_unit": business_unit,
    "closedate": renewal_date.valueOf(), // Renewal Deal new close date by adding deal length to the original deal close date
    "renew_date": new_renewal_date.valueOf(), // Renewal Deal new close date by adding deal length to the original deal close date    "business_unit": business_unit,
    "deal_currency_code": deal_currency_code,
    "description": description,
    "dealname": new_dealname,
    "hs_analytics_source": hs_analytics_source,
    "autorenew_duration": autorenew_duration, // Renewal deal new length. By default, same length as the original deal
    "hubspot_owner_id": hubspot_owner_id, // Renewal deal owner. The value used here is the ID of the CSM responsible for renewal. You'll use your own value generated in your portal
    "success_owner_id": success_owner_id,
    "contract_owner_id": contract_owner_id,
    "pipeline": pipeline, // Renewal Deal pipeline. The value used here is the ID of the renewal pipeline. You'll use your own value generated in your portal
    "dealstage": dealstage, // Renewal Deal stage
    "parent_id": hs_object_id,
  };

Creating the deal without associations

Now that we have the properties, we will create the new deal and want the ID of the new deal back. So we declare the NewDealID and use the create endpoint of the deals API to create just that. We will use await and .then to get the response, and return just the ID.
We could use a console.log at this point to verify everything is working, but once it is you may want to comment that out or remove it.

// Create the deal
const NewDealID = await hubspotClient.crm.deals.basicApi.create({ properties })
  .then(DealCreateResponse => {
    return (DealCreateResponse.id);
  });
  //    console.log("New Deal ID is " + NewDealID);

Get primary company of the enrolled deal

We should keep in mind here that you can make custom code work the way you want to as long as there is logics. What we decided to do here is just re-associate the primary company. We create a JS object called 'AssociatedPrimaryCompany' and await the result of an API call to the associations API that retrieves the complete association data for the enrolled object where the associationstype is 'company'.
Do note that we use 'event.object.objectId' which gets the ID of the enrolled object. We could also have used 'hs_object_id' but wanted to showcase you can get info directly of the enrolled object as well.

The first company we get back should be the primary company. But we are not taking any chances and will find the company with the label 'Primary' and return just that one.

If there is no primary company it will log that to the console. This should not happen in a B2B environment and we encourage you to use automation to check that as well.

  // get primary company associations
    const AssociatedPrimaryCompany = await hubspotClient.crm.deals.associationsApi.getAll(event.object.objectId, 'company')
    .then(AssociatedCompaniesResponse => {
      const el = AssociatedCompaniesResponse.results.find(({ associationTypes }) => associationTypes.find(({ label }) => label === 'Primary')) 
        if (el) {
          return el.toObjectId;
        } else {
          console.log('Primary company not found', AssociatedCompaniesResponse.results)
        }
    });

Write the primary company association to the cloned deal

We will use the same API with the create endpoint and use the 'NewDealID' to create a company association. In this case the associationTypeId is a common one and can be retrieved from the list in the documentation. In the line items workflow explanation we will look into how we can retrieve that for non-common association types.

 hubspotClient.crm.deals.associationsApi.create(
      NewDealID,
      'companies',
      AssociatedPrimaryCompany,
      [
          {
                "associationCategory": "HUBSPOT_DEFINED",
                "associationTypeId": 5
          }
      ]
  )

Get all associated contacts

We start here almost identical to the way we get the company associations. The main difference is that we will in most cases return multiple contacts and therefore map them.
One thing to note is that at the time of writing there is no contact to deal association label.

  // get contacts associations
  const AssociatedContacts = await hubspotClient.crm.deals.associationsApi.getAll(event.object.objectId, 'contact')
  .then(AssociatedContactsResponse => {
    return AssociatedContactsResponse.results.map(({ toObjectId }) => String(toObjectId))
  }); 

Write all associated contacts to the cloned deal

Being mindful that we will likely write a batch of contacts to the associations API we will use the BatchApi endpoint of the associations API.
This requires an array that we call 'inputs, which contains the from, to and (association)type. As 'from' is not available in javascript, we must escape that by adding an '_' before that.

 // write contact associations to deal.
  const inputs = AssociatedContacts.map(id => ({
    _from: { id: NewDealID }, // please note the _ before 'from'!!!
    to: { id },
    type: 'deal_to_contact'
  }))
  const BatchInputPublicAssociationContacts = { inputs };
  
  try {  
    await hubspotClient.crm.associations.batchApi.create("Deals", "Contacts", BatchInputPublicAssociationContacts);
  } 
    
  catch (e) {
    e.message === 'HTTP request failed'
      ? console.error(JSON.stringify(e.response, null, 2))
      : console.error(e)
  }

Data output and wrapping up

Last but not least we will set up the output fields. In our workflow we will just pass the NewDealID end close everything after that.

  // Pass information back as a data output to use as a data input with Copy to Property workflow action.
  callback({ 
    outputFields: {
      NewDealID: NewDealID,
    }
  }); // end callback  
}

And don't forget to actually pass the outputField back to the workflow

Data_Outputs_Clone_Deal

In our case we will write back the NewDealID to the original deal. Sure, we could have done that in the custom code. But this is cleaner and faster.

Workflow_copy_NewDealID

Completed code

This custom-coded action enables you to automate the process of cloning a deal when it reaches a certain stage in your pipeline. With a few tweaks, you can adjust the code to suit your specific business needs and further streamline your deal management processes in HubSpot.

Remember, while this guide provides a detailed explanation of the code, implementing it requires a decent understanding of JavaScript and the HubSpot API. If you are not comfortable with these, consider seeking assistance from a developer or a HubSpot solutions partner.

Overall, this custom-coded action helps you leverage the power of automation in HubSpot, allowing you to focus on more strategic tasks and enhancing your overall deal management efficiency. Happy coding!

// settings for this custom code:
const pipeline = 22679326; // new pipeline
const dealstage = 53954819; // new dealstage

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

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

  //Retrieving the included properties 
  const autorenew = Boolean(event.inputFields['AutoRenew']);  // custom property!!!
  const hubspot_owner_id = Number(event.inputFields['hubspot_owner_id']);
  const business_unit = event.inputFields['business_unit'];    // custom property!!! No need for this if you don't use business units on a deal level.
  const deal_currency_code = event.inputFields['deal_currency_code'];
  const description = event.inputFields['description'];
  const dealname = event.inputFields['dealname'];
  const hs_analytics_source = event.inputFields['hs_analytics_source'];
  const renewal_date = Number(event.inputFields['renewal_date']);
  const autorenew_duration = Number(event.inputFields['autorenew_duration']);  // custom property!!!
  const hs_mrr = Number(event.inputFields['hs_mrr']);
  const success_owner_id = event.inputFields['success_owner_id'];  // custom property!!!
  const contract_owner_id = event.inputFields['contract_owner_id'];  // custom property!!!
  const hs_object_id = event.inputFields['hs_object_id'];

  // Amend some of the properties
  const new_renewal_date = new Date(renewal_date)
	new_renewal_date.setMonth(new_renewal_date.getMonth() + autorenew_duration)

  // calculate new ammount based on MRR
  const new_amount = hs_mrr * autorenew_duration; // could do with a bit more logics.

  // remove part of name after ' - ' and add renewal.
  const new_dealname =  dealname.split(" - ", 1) + " - Renewal";

  //Set properties for the renewal deal        
  const properties = {
    "amount": new_amount,
    "autorenew": autorenew,
    "business_unit": business_unit,
    "closedate": renewal_date.valueOf(), // Renewal Deal new close date by adding deal length to the original deal close date
    "renew_date": new_renewal_date.valueOf(), // Renewal Deal new close date by adding deal length to the original deal close date    "business_unit": business_unit,
    "deal_currency_code": deal_currency_code,
    "description": description,
    "dealname": new_dealname,
    "hs_analytics_source": hs_analytics_source,
    "autorenew_duration": autorenew_duration, // Renewal deal new length. By default, same length as the original deal
    "hubspot_owner_id": hubspot_owner_id, // Renewal deal owner. The value used here is the ID of the CSM responsible for renewal. You'll use your own value generated in your portal
    "success_owner_id": success_owner_id,
    "contract_owner_id": contract_owner_id,
    "pipeline": pipeline, // Renewal Deal pipeline. The value used here is the ID of the renewal pipeline. You'll use your own value generated in your portal
    "dealstage": dealstage, // Renewal Deal stage
    "parent_id": hs_object_id,
  };

// Create the deal
const NewDealID = await hubspotClient.crm.deals.basicApi.create({ properties })
  .then(DealCreateResponse => {
    return (DealCreateResponse.id);
  });
  //    console.log("New Deal ID is " + NewDealID);  
  
  // get primary company associations
    const AssociatedPrimaryCompany = await hubspotClient.crm.deals.associationsApi.getAll(event.object.objectId, 'company')
    .then(AssociatedCompaniesResponse => {
      const el = AssociatedCompaniesResponse.results.find(({ associationTypes }) => associationTypes.find(({ label }) => label === 'Primary')) 
        if (el) {
          return el.toObjectId;
        } else {
          console.log('Primary company not found', AssociatedCompaniesResponse.results)
        }
    });

  // create primary company association
  // associationTypeId list can be found on: https://legacydocs.hubspot.com/docs/methods/crm-associations/crm-associations-overview
  hubspotClient.crm.deals.associationsApi.create(
      NewDealID,
      'companies',
      AssociatedPrimaryCompany,
      [
          {
                "associationCategory": "HUBSPOT_DEFINED",
                "associationTypeId": 5
          }
      ]
  )
  
  // get contacts associations
  const AssociatedContacts = await hubspotClient.crm.deals.associationsApi.getAll(event.object.objectId, 'contact')
  .then(AssociatedContactsResponse => {
    return AssociatedContactsResponse.results.map(({ toObjectId }) => String(toObjectId))
  });  
  
  // write contact associations to deal.
  const inputs = AssociatedContacts.map(id => ({
    _from: { id: NewDealID }, // please note the _ before 'from'!!!
    to: { id },
    type: 'deal_to_contact'
  }))
  const BatchInputPublicAssociationContacts = { inputs };
  
  try {  
    await hubspotClient.crm.associations.batchApi.create("Deals", "Contacts", BatchInputPublicAssociationContacts);
  } 
  
  catch (e) {
    e.message === 'HTTP request failed'
      ? console.error(JSON.stringify(e.response, null, 2))
      : console.error(e)
  }
  
  // Pass information back as a data output to use as a data input with Copy to Property workflow action.
  callback({ 
    outputFields: {
      NewDealID: NewDealID,
    }
  }); // end callback  
}