Sync HubSpot form submissions to Salesforce in Jitterbit Studio
Introduction
HubSpot form submissions arrive as a paginated list in which each submission contains an array of name/value pairs (one pair per form field) rather than a flat record with named properties. This guide shows how to retrieve those submissions using the HubSpot Forms Submissions API via the HTTP v2 connector, dispatch them one at a time through Studio's SelectNodes loop pattern, extract the email address, first name, and last name from the values array, check Salesforce for an existing lead, and create or update a Lead record based on the result.
This guide uses:
- The HTTP v2 connector to call the HubSpot Forms Submissions API.
- Studio temporary storage to hold the full submission batch between the fetch step and the per-record dispatch loop.
SelectNodesandReadFileto iterate through stored XML records.SfLookupAllto detect duplicate leads by email address.- The Salesforce connector's Create and Update activities to write the Lead record.
This guide assumes a HubSpot account with a private app and an access token. You need the GUID of the form whose submissions you want to sync. Each HubSpot form has a unique GUID visible in the form editor URL or via the HubSpot Forms API.
Design pattern
The integration runs in two stages: a batch fetch that retrieves all submissions for one form and stores them as XML, and a per-record loop that processes each submission individually.
Initialize variables
Set form path"] --> B["HTTP v2 GET
HubSpot submissions
for [$hubspot.form.guid]"] B --> C["Transformation
Write to
temp storage (batch)"] C --> D["Script
ReadFile + SelectNodes
loop over submissions"] D --> E["Per-record operation
(transformation + scripts)"] E --> F{"Email found
in Salesforce?"} F -- Yes --> G["Salesforce
Update Lead"] F -- No --> H["Salesforce
Create Lead
+ optional ZoomInfo enrich"]
The batch fetch runs once per form sync. The per-record loop runs once per submission.
Part 1: Configure project variables and the HTTP v2 connection
Step 1: Create project variables
Store the HubSpot access token and form GUID as project variables so they can be updated without editing scripts. Open the project actions menu and select Project Variables. Then add:
| Name | Default value | Description |
|---|---|---|
hubspot.access.token |
(your HubSpot private app access token) | Bearer token used to authenticate all API calls |
hubspot.form.guid |
(your HubSpot form GUID) | GUID of the form whose submissions to sync |
Mark hubspot.access.token as hidden. For guidance on storing credentials securely, see Manage endpoint credentials.
Step 2: Configure the HTTP v2 connection
-
In Studio, open your project and click the Project endpoints and connectors tab in the design component palette.
-
Click the HTTP v2 connector to open the connection configuration screen.
-
Connection Name: Enter
HubSpot API. -
Base URL: Enter
https://api.hubapi.com. -
Authorization: Select Bearer Token and enter
[$hubspot.access.token]as the token value. -
Click Test to verify the connection, then click Save Changes.
Step 3: Create the GET Forms Submissions activity
-
From the HubSpot API connection, drag a GET activity onto the design canvas.
-
Name: Enter
HubSpot - GET Form Submissions. -
Path: Enter
/form-integrations/v1/submissions/forms/[$hubspot.form.guid].Studio substitutes
[$hubspot.form.guid]with the project variable value at runtime. To sync a different form, update the project variable without modifying the activity configuration. -
In the Request tab, add one header:
- Key:
Content-Type - Value:
application/json
- Key:
-
Click Finished.
Part 2: Retrieve submissions and write them to temporary storage
Step 1: Create the fetch operation
Create an operation named HubSpot - Fetch Form Submissions with these steps in order:
-
Script: Initialize the variables used in per-record processing:
// Reset per-run staging variables $hubspot.email = ""; $hubspot.firstName = ""; $hubspot.lastName = ""; WriteToOperationLog("Starting HubSpot form submission sync for form: " + [$hubspot.form.guid]); -
GET activity: Place the
HubSpot - GET Form Submissionsactivity as the source step. -
Transformation: Add a transformation after the GET activity with a Temporary Storage Write activity as the target. Map the source JSON schema to the target schema at the
results/itemandresults/item/values/itemloop levels so that all submission records are written to temporary storage in a single pass.Name the transformation
HubSpot - Write Submissions to Temp Storageand name the Temporary Storage Write activityWrite HubSpot Submissions.
The Temporary Storage Write activity stores the submission records as XML. Studio writes each item at the innermost loop level as a DocInfo element, which is the format the dispatch loop reads in Part 3.
Step 2: Handle the case where no submissions are found
Add a Script step after the transformation. Check the submission count before dispatching the loop:
$count = Length(SelectNodes(
ReadFile("<TAG>activity:tempstorage/HubSpot Temp Storage/tempstorage_read/Read HubSpot Submissions</TAG>"),
"//DocInfo"
));
If($count == 0,
WriteToOperationLog("No HubSpot submissions found. Exiting.");
CancelOperation("<TAG>operation:HubSpot - Fetch Form Submissions</TAG>");
,
WriteToOperationLog("Found " + $count + " submission(s) to process.");
);
CancelOperation cancels the current operation cleanly without raising an error when no submissions are present.
Part 3: Dispatch submissions one at a time
Step 1: Add the dispatch script
Add a Script step to the HubSpot - Fetch Form Submissions operation after the count check. This script reads the stored batch, extracts individual submission records using SelectNodes, and calls a per-record operation once per submission:
data = ReadFile("<TAG>activity:tempstorage/HubSpot Temp Storage/tempstorage_read/Read HubSpot Submissions</TAG>");
nodes = SelectNodes(data, "//DocInfo");
counts = Length(nodes);
WriteToOperationLog("Dispatching " + counts + " submission(s).");
i = 0;
While(i < counts,
node = nodes[i];
// Wrap the single node so the per-record operation has a valid XML source
xml = "<SubmissionBatch><Submissions>" + String(node) + "</Submissions></SubmissionBatch>";
WriteFile(
"<TAG>activity:tempstorage/HubSpot Temp Storage 2/tempstorage_write/Write Single HubSpot Submission</TAG>",
xml
);
FlushFile(
"<TAG>activity:tempstorage/HubSpot Temp Storage 2/tempstorage_write/Write Single HubSpot Submission</TAG>"
);
RunOperation("<TAG>operation:HubSpot - Process Single Submission</TAG>");
i = i + 1;
);
The <SubmissionBatch><Submissions> wrapper is a fixed outer container that gives the per-record operation's source transformation a predictable root element. The extracted DocInfo node becomes the inner element that the transformation maps.
FlushFile forces the write to complete before RunOperation calls the per-record operation. Without flushing, the operation may read the previous record's data.
Step 2: Set up temporary storage for per-record dispatch
Configure a second Temporary Storage endpoint (for example, HubSpot Temp Storage 2) with:
- A Write activity named
Write Single HubSpot Submission. - A Read activity named
Read Single HubSpot Submission, used as the source in the per-record operation.
The two endpoints keep the full batch and the per-record buffer separate so that the dispatch loop does not overwrite the source data it is iterating.
Part 4: Extract field values from the submission
HubSpot form submissions store field values as a values array of name/value objects. The field name (email, firstname, lastname) is in the name field, and the submitted value is in the value field. The transformation must iterate both levels (results/item for each submission and the nested values/item for each field) and read the name field to know which variable to populate.
Step 1: Create the per-record operation
Create an operation named HubSpot - Process Single Submission. Its source is the Read Single HubSpot Submission Temporary Storage Read activity.
Step 2: Create the field extraction transformation
Add a transformation to the per-record operation named HubSpot - Extract Submission Fields. Define two loop levels in the transformation:
- Outer loop:
results/itemin the source, mapping to the corresponding target path. - Inner loop:
results/item/values/itemin the source, nested within the outer loop.
In the inner loop, add these mapping scripts:
For the values/item/name target node:
$hubspot.fieldName = json$results$item.values$item.name$
For the values/item/value target node:
If($hubspot.fieldName == "email",
$hubspot.email = json$results$item.values$item.value$
);
If($hubspot.fieldName == "firstname",
$hubspot.firstName = json$results$item.values$item.value$
);
If($hubspot.fieldName == "lastname",
$hubspot.lastName = json$results$item.values$item.value$
);
For the outer results/item/pageUrl target node (which fires after the inner loop completes for each submission), call the Salesforce check operation:
If(Length(Trim($hubspot.email)) > 0,
$hubspot.email = ToLower(Trim($hubspot.email));
RunOperation("<TAG>operation:HubSpot - Check Salesforce Lead</TAG>");
,
WriteToOperationLog("Submission missing email field. Skipping.");
);
The pageUrl mapping runs once per submission record, after all its values/item children have been processed. This makes it the correct place to trigger the downstream Salesforce operation. Normalizing the email to lowercase before the lookup prevents case-sensitive mismatches against Salesforce records.
Part 5: Check Salesforce for an existing lead
Step 1: Create the Salesforce check operation
Create an operation named HubSpot - Check Salesforce Lead. Add a Script step as the only operation step:
$sf.existingLeadId = "";
$result = SfLookupAll(
"<TAG>endpoint:salesforce/Salesforce</TAG>",
"SELECT Id FROM Lead WHERE Email = '"
+ $hubspot.email
+ "' AND IsConverted = false LIMIT 1"
);
If(Length($result) > 0,
$sf.existingLeadId = $result[0][0];
WriteToOperationLog("Existing lead found: " + $sf.existingLeadId);
RunOperation("<TAG>operation:HubSpot - Update Salesforce Lead</TAG>");
,
WriteToOperationLog("No existing lead for: " + $hubspot.email);
RunOperation("<TAG>operation:HubSpot - Create Salesforce Lead</TAG>");
);
SfLookupAll returns a two-dimensional array: each inner array is one result row, and each element is a field value in the order listed in the SELECT clause. $result[0][0] is the Id field from the first (and only) row returned. LIMIT 1 prevents multiple matches from causing an array index error when the same email appears in more than one lead record.
IsConverted = false excludes leads that have already been converted to contacts, opportunities, or accounts. Updating a converted lead in Salesforce raises an error, so it is safer to skip them and let the create path handle the edge case separately if needed.
For the SOQL query pattern, see Query Salesforce records using SOQL.
Part 6: Create or update the Salesforce Lead
Step 1: Create the Lead record
Create an operation named HubSpot - Create Salesforce Lead. Add a Transformation step that maps the staging variables to a Salesforce Create activity targeting the Lead object:
| Source expression | Salesforce Lead field |
|---|---|
$hubspot.firstName |
FirstName |
$hubspot.lastName |
LastName |
$hubspot.email |
Email |
"HubSpot" (literal) |
LeadSource |
Set a literal value for LeadSource to tag all leads created by this integration for reporting purposes.
The Salesforce Create activity returns the new record ID. Capture it for use in the optional enrichment step:
$sf.newLeadId = TrimChars(
GetJSONString($jitterbit.response, "/id"),
"\""
);
WriteToOperationLog("Created Salesforce lead: " + $sf.newLeadId);
After capturing the ID, call the ZoomInfo enrichment operation to fill in additional fields (see Part 7):
If(Length($sf.newLeadId) > 0,
RunOperation("<TAG>operation:ZoomInfo - Enrich Lead</TAG>")
);
Step 2: Update the existing Lead record
Create an operation named HubSpot - Update Salesforce Lead. Add a Transformation step that maps to a Salesforce Update activity targeting the Lead object. The Update activity requires the Id field to identify the record:
| Source expression | Salesforce Lead field |
|---|---|
$sf.existingLeadId |
Id |
$hubspot.firstName |
FirstName |
$hubspot.lastName |
LastName |
$hubspot.email |
Email |
Map only the fields the integration owns. Omitting fields from the transformation preserves existing values on the Salesforce record.
Part 7: Enrich new leads with ZoomInfo
After creating a new Salesforce lead, an optional enrichment step uses ZoomInfo to fill in the contact's current job title, company, and direct phone number. The enrichment operation calls the ZoomInfo enrich endpoint using the lead email and company name as lookup keys, then updates the Salesforce lead with the returned data.
For the ZoomInfo API connection setup and the resource router pattern used by this step, see Enrich contact data using ZoomInfo.
Verify the integration
-
In HubSpot, open the form you are syncing and submit a test entry with a unique email address that does not exist in Salesforce. Run
HubSpot - Fetch Form Submissionsmanually and check the operation logs. Confirm that the log shows the correct submission count and thatHubSpot - Create Salesforce Leadran for the test submission. -
In Salesforce, search for the Lead record by the test email address. Confirm that
FirstName,LastName, andEmailare populated and thatLeadSourceis set toHubSpot. -
Submit a second test entry using the same email address. Run the fetch operation again and confirm that
HubSpot - Update Salesforce Leadran instead of the create operation, and that the existing Salesforce lead was updated rather than duplicated. -
Test the empty-submission case by running the operation against a form with no submissions. Confirm that the operation logs show "No HubSpot submissions found" and that no Salesforce operations were called.
-
To test a submission missing the email field, temporarily add a test submission without an email. Confirm that the log shows "Submission missing email field. Skipping." and that no Salesforce operations ran for that record.
-
If the GET activity returns a 401 error, confirm that
[$hubspot.access.token]is set and that the private app access token has not expired. HubSpot private app tokens do not expire by default, but they can be rotated. Check the token in the HubSpot private app settings. -
If
SelectNodesreturns zero nodes despite the GET activity returning data, confirm that the transformation in Part 2 is looping at theresults/itemlevel and that the Temporary Storage Write activity completed before the dispatch script ran.