The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"updatedAt": "2026-05-25T15:39:54.908Z",
"createdAt": "2026-05-22T16:57:37.438Z",
"id": "DycMMqpsFubSr8ar",
"name": "[QUO] Bidirectional Contact Sync",
"description": null,
"active": true,
"isArchived": false,
"nodes": [
{
"id": "quo-webhook",
"name": "Quo Call Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
0
],
"parameters": {
"httpMethod": "POST",
"path": "quo-call-sync",
"responseMode": "responseNode",
"options": {}
},
"onError": "continueRegularOutput"
},
{
"id": "respond-200",
"name": "Respond 200",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
240,
0
],
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify({ received: true }) }}",
"options": {
"responseCode": 200
}
}
},
{
"id": "parse-quo",
"name": "Parse Quo Payload",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
480,
0
],
"parameters": {
"jsCode": "// Purpose: Extract call data from Quo API v3 webhook\n// Inputs: Quo webhook body with data.object containing from/to/direction\n// Outputs: phone (contact's number), callId, direction\n\nconst body = $input.first().json.body || $input.first().json;\nconst callObj = (body.data && body.data.object) || {};\n\nconst callId = callObj.id || null;\nconst direction = callObj.direction || '';\nconst from = callObj.from || null;\nconst to = callObj.to || null;\n\n// Contact's phone: outgoing = we called them (to), incoming = they called us (from)\nconst phone = direction === 'outgoing' ? to : from;\n\nreturn [{ json: { phone, callId, direction } }];"
}
},
{
"id": "has-phone",
"name": "Has Phone?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
720,
0
],
"parameters": {
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "check-phone",
"leftValue": "={{ $json.phone }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
]
},
"options": {}
}
},
{
"id": "normalize-phone",
"name": "Normalize Phone",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
960,
0
],
"parameters": {
"jsCode": "// Purpose: Normalize raw phone from webhook for HubSpot search and formatting\n// Inputs: phone (raw E.164 from Quo webhook)\n// Outputs: phone, phoneNormalized (10 digits), phoneFormatted (+1 (xxx) xxx-xxxx)\n\nconst raw = $json.phone || '';\nconst digits = raw.replace(/[^0-9]/g, '');\nconst phoneNormalized = (digits.length === 11 && digits.startsWith('1')) ? digits.slice(1) : digits;\nconst phoneFormatted = phoneNormalized.length === 10\n ? '+1 (' + phoneNormalized.slice(0,3) + ') ' + phoneNormalized.slice(3,6) + '-' + phoneNormalized.slice(6)\n : raw;\n\nreturn [{ json: { phone: raw, phoneNormalized, phoneFormatted } }];"
}
},
{
"id": "fetch-quo-contact",
"name": "Fetch Quo Contact",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1200,
0
],
"parameters": {
"jsCode": "// Purpose: Find Quo contact by phone via pagination (filter doesn't work)\n// Inputs: Normalize Phone (raw phone + 10-digit base)\n// Outputs: { data: [matched contact] } matching Quo API response shape\n\nconst normData = $('Normalize Phone').first().json;\nconst searchBase = normData.phoneNormalized || '';\n\nconst toBase = (p) => {\n const d = (p || '').replace(/[^0-9]/g, '');\n return (d.length === 11 && d.startsWith('1')) ? d.slice(1) : d;\n};\n\nconst QUO_API_KEY = 'PMdBWaig1oFVhOBinL42xxAdKlqxfuSX';\nconst MAX_PAGES = 60;\n\nlet pageToken = null;\nlet matched = null;\nlet pagesScanned = 0;\nlet totalScanned = 0;\nlet lastError = null;\n\nfor (let i = 0; i < MAX_PAGES; i++) {\n pagesScanned++;\n let url = 'https://api.openphone.com/v1/contacts?maxResults=50';\n if (pageToken) url += '&pageToken=' + encodeURIComponent(pageToken);\n\n let response;\n try {\n response = await this.helpers.httpRequest({\n method: 'GET',\n url: url,\n headers: { 'Authorization': QUO_API_KEY },\n json: true\n });\n } catch (err) {\n lastError = (err && err.message) || String(err);\n break;\n }\n\n const contacts = Array.isArray(response && response.data) ? response.data : [];\n totalScanned += contacts.length;\n\n for (const c of contacts) {\n const phones = Array.isArray(c && c.defaultFields && c.defaultFields.phoneNumbers) ? c.defaultFields.phoneNumbers : [];\n if (phones.some(p => toBase(p.value) === searchBase)) {\n matched = c;\n break;\n }\n }\n if (matched) break;\n\n pageToken = response && response.nextPageToken;\n if (!pageToken) break;\n}\n\nreturn [{ json: { data: matched ? [matched] : [], pagesScanned, totalScanned, matched: !!matched, lastError } }];"
},
"onError": "continueRegularOutput"
},
{
"id": "normalize-contact",
"name": "Normalize Contact Data",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1440,
0
],
"parameters": {
"jsCode": "// Purpose: Extract fields from matched Quo contact\n// Inputs: Fetch Quo Contact (data: [matched]), Normalize Phone\n// Outputs: firstName, lastName, company, phone, phoneNormalized, phoneFormatted\n\nconst quoResult = $input.first().json;\nconst normData = $('Normalize Phone').first().json;\nconst rawPhone = normData.phone || '';\nconst searchBase = normData.phoneNormalized || '';\n\nconst allContacts = Array.isArray(quoResult.data) ? quoResult.data : [];\nconst matched = allContacts[0] || null;\n\nconst fields = matched ? (matched.defaultFields || {}) : {};\nconst firstName = fields.firstName || '';\nconst lastName = fields.lastName || '';\nconst company = fields.company || '';\n\nreturn [{ json: { firstName, lastName, company, phone: rawPhone, phoneNormalized: searchBase, phoneFormatted: normData.phoneFormatted } }];"
}
},
{
"id": "search-hs-phone",
"name": "Search HS by Phone",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1680,
0
],
"parameters": {
"method": "POST",
"url": "https://api.hubapi.com/crm/v3/objects/contacts/search",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ filterGroups: [{ filters: [{ propertyName: 'phone', operator: 'EQ', value: $json.phoneNormalized }] }, { filters: [{ propertyName: 'phone', operator: 'EQ', value: '+1' + $json.phoneNormalized }] }, { filters: [{ propertyName: 'phone', operator: 'EQ', value: $json.phoneFormatted }] }, { filters: [{ propertyName: 'mobilephone', operator: 'EQ', value: $json.phoneNormalized }] }, { filters: [{ propertyName: 'mobilephone', operator: 'EQ', value: '+1' + $json.phoneNormalized }] }], properties: ['firstname','lastname','phone','mobilephone'], limit: 1 }) }}",
"options": {}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"onError": "continueRegularOutput"
},
{
"id": "contact-exists",
"name": "Contact Exists?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1920,
0
],
"parameters": {
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "check-total",
"leftValue": "={{ $json.total }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "gt"
}
}
]
},
"options": {}
}
},
{
"id": "update-hs-contact",
"name": "Update HS Contact",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2160,
-140
],
"parameters": {
"method": "PATCH",
"url": "=https://api.hubapi.com/crm/v3/objects/contacts/{{ $json.results[0].id }}",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ properties: { phone: $('Normalize Phone').first().json.phoneFormatted } }) }}",
"options": {}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"onError": "continueRegularOutput"
},
{
"id": "set-contact-id",
"name": "Set Contact ID",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
2400,
-140
],
"parameters": {
"assignments": {
"assignments": [
{
"id": "1",
"name": "hsContactId",
"value": "={{ $json.id }}",
"type": "string"
}
]
},
"options": {}
}
},
{
"id": "has-company",
"name": "Has Company?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
2640,
0
],
"parameters": {
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "check-company",
"leftValue": "={{ $('Normalize Contact Data').first().json.company }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
]
},
"options": {}
}
},
{
"id": "search-hs-company",
"name": "Search HS Company",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2880,
-100
],
"parameters": {
"method": "POST",
"url": "https://api.hubapi.com/crm/v3/objects/companies/search",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ filterGroups: [{ filters: [{ propertyName: 'name', operator: 'EQ', value: $('Normalize Contact Data').first().json.company }] }], properties: ['name'], limit: 1 }) }}",
"options": {}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"onError": "continueRegularOutput"
},
{
"id": "company-exists",
"name": "Company Exists?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
3120,
-100
],
"parameters": {
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "check-company-total",
"leftValue": "={{ $json.total }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "gt"
}
}
]
},
"options": {}
}
},
{
"id": "set-company-id",
"name": "Set Company ID",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
3360,
-220
],
"parameters": {
"assignments": {
"assignments": [
{
"id": "1",
"name": "hsCompanyId",
"value": "={{ $json.results[0].id }}",
"type": "string"
}
]
},
"options": {}
}
},
{
"id": "create-hs-company",
"name": "Create HS Company",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
3360,
20
],
"parameters": {
"method": "POST",
"url": "https://api.hubapi.com/crm/v3/objects/companies",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ properties: { name: $('Normalize Contact Data').first().json.company } }) }}",
"options": {}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"onError": "continueRegularOutput"
},
{
"id": "set-new-company-id",
"name": "Set New Company ID",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
3600,
20
],
"parameters": {
"assignments": {
"assignments": [
{
"id": "1",
"name": "hsCompanyId",
"value": "={{ $json.id }}",
"type": "string"
}
]
},
"options": {}
}
},
{
"id": "get-all-ids",
"name": "Get All IDs",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3840,
-100
],
"parameters": {
"jsCode": "// Purpose: Collect hsContactId + hsCompanyId for association\n// Inputs: Set Company ID or Set New Company ID, plus either Set Contact ID or Set New Contact ID\n// Outputs: hsContactId, hsCompanyId\n\nconst hsCompanyId = $input.first().json.hsCompanyId;\n\nlet hsContactId = null;\ntry { const v = $('Set Contact ID').first().json.hsContactId; if (v) hsContactId = v; } catch(e) {}\nif (!hsContactId) {\n try { const v = $('Set New Contact ID').first().json.hsContactId; if (v) hsContactId = v; } catch(e) {}\n}\n\nif (!hsContactId || !hsCompanyId) {\n throw new Error('Cannot associate - missing IDs. contactId=' + hsContactId + ', companyId=' + hsCompanyId);\n}\n\nreturn [{ json: { hsContactId, hsCompanyId } }];"
}
},
{
"id": "associate",
"name": "Associate Contact + Company",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
4080,
-100
],
"parameters": {
"method": "PUT",
"url": "=https://api.hubapi.com/crm/v3/objects/contacts/{{ $json.hsContactId }}/associations/companies/{{ $json.hsCompanyId }}/contact_to_company",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"options": {}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"onError": "continueRegularOutput"
},
{
"id": "quo-contact-webhook",
"name": "Quo Contact Updated Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
700
],
"parameters": {
"httpMethod": "POST",
"path": "quo-contact-updated",
"responseMode": "responseNode",
"options": {}
},
"onError": "continueRegularOutput"
},
{
"id": "respond-200-c2",
"name": "Respond 200 (Contact)",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
240,
700
],
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify({ received: true }) }}",
"options": {
"responseCode": 200
}
}
},
{
"id": "parse-quo-contact",
"name": "Parse Quo Contact Update",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
480,
700
],
"parameters": {
"jsCode": "// Purpose: Extract contact fields from Quo contact.updated webhook\n// Inputs: Quo webhook body \u2014 firstName/lastName/company directly on data.object, phone at data.object.fields.Phone\n// Outputs: firstName, lastName, company, phone, email, quoContactId\n\nconst body = $input.first().json.body || $input.first().json;\nconst obj = (body.data && body.data.object) || {};\n\nconst firstName = obj.firstName || '';\nconst lastName = obj.lastName || '';\nconst company = obj.company || '';\nconst phone = (obj.fields && obj.fields.Phone) || '';\nconst email = (obj.fields && obj.fields.Email) || '';\nconst quoContactId = obj.id || '';\n\nreturn [{ json: { firstName, lastName, company, phone, email, quoContactId } }];"
},
"onError": "continueRegularOutput"
},
{
"id": "has-phone-c2",
"name": "Has Phone? (Contact)",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
720,
700
],
"parameters": {
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "check-phone-c2",
"leftValue": "={{ $json.phone }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
]
},
"options": {}
}
},
{
"id": "normalize-phone-c2",
"name": "Normalize Phone (Contact)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
960,
700
],
"parameters": {
"jsCode": "// Purpose: Normalize raw phone from Quo contact for HubSpot search\n// Inputs: phone (raw E.164), all Quo contact fields in $json\n// Outputs: all Quo fields carried forward, phoneNormalized (10 digits), phoneFormatted\n\nconst raw = $json.phone || '';\nconst digits = raw.replace(/[^0-9]/g, '');\nconst phoneNormalized = (digits.length === 11 && digits.startsWith('1')) ? digits.slice(1) : digits;\nconst phoneFormatted = phoneNormalized.length === 10\n ? '+1 (' + phoneNormalized.slice(0,3) + ') ' + phoneNormalized.slice(3,6) + '-' + phoneNormalized.slice(6)\n : raw;\n\nreturn [{ json: { ...$json, phoneNormalized, phoneFormatted } }];"
}
},
{
"id": "search-hs-phone-c2",
"name": "Search HS by Phone (Contact)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1200,
700
],
"parameters": {
"method": "POST",
"url": "https://api.hubapi.com/crm/v3/objects/contacts/search",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ filterGroups: [{ filters: [{ propertyName: 'phone', operator: 'EQ', value: $json.phoneNormalized }] }, { filters: [{ propertyName: 'phone', operator: 'EQ', value: '+1' + $json.phoneNormalized }] }, { filters: [{ propertyName: 'phone', operator: 'EQ', value: $json.phoneFormatted }] }, { filters: [{ propertyName: 'mobilephone', operator: 'EQ', value: $json.phoneNormalized }] }, { filters: [{ propertyName: 'mobilephone', operator: 'EQ', value: '+1' + $json.phoneNormalized }] }], properties: ['firstname','lastname','company','phone','mobilephone','email'], limit: 1 }) }}",
"options": {}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"onError": "continueRegularOutput"
},
{
"id": "hs-found-c2",
"name": "HS Contact Found?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1440,
700
],
"parameters": {
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "check-hs-total-c2",
"leftValue": "={{ $json.total }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "gt"
}
}
]
},
"options": {}
}
},
{
"id": "build-hs-patch",
"name": "Build HS Patch",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1680,
560
],
"parameters": {
"jsCode": "// Purpose: Compare Quo contact fields with current HubSpot values; build minimal patch\n// Inputs: HS search result (results[0].properties), Normalize Phone (Contact) for Quo source data\n// Outputs: properties (changed fields only), hsContactId, changeCount\n\nconst hsResult = $input.first().json;\nconst hsContact = (hsResult.results && hsResult.results[0]) || {};\nconst hsProps = hsContact.properties || {};\nconst quoData = $('Normalize Phone (Contact)').first().json;\n\nconst candidates = [\n ['firstname', quoData.firstName],\n ['lastname', quoData.lastName],\n ['company', quoData.company],\n ['phone', quoData.phoneFormatted],\n ['email', quoData.email]\n];\n\nconst props = {};\nfor (const [key, val] of candidates) {\n if (val && (hsProps[key] || '') !== val) props[key] = val;\n}\n\nreturn [{ json: { properties: props, hsContactId: hsContact.id, changeCount: Object.keys(props).length } }];"
}
},
{
"id": "has-changes-c2",
"name": "Has Changes? (Quo->HS)",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1920,
560
],
"parameters": {
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "check-changes-c2",
"leftValue": "={{ $json.changeCount }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "gt"
}
}
]
},
"options": {}
}
},
{
"id": "patch-hs-c2",
"name": "Patch HS Contact",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2160,
560
],
"parameters": {
"method": "PATCH",
"url": "=https://api.hubapi.com/crm/v3/objects/contacts/{{ $json.hsContactId }}",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ properties: $json.properties }) }}",
"options": {}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"onError": "continueRegularOutput"
},
{
"id": "hs-contact-webhook",
"name": "HS Contact Change Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
1300
],
"parameters": {
"httpMethod": "POST",
"path": "hs-contact-change",
"responseMode": "responseNode",
"options": {}
},
"onError": "continueRegularOutput"
},
{
"id": "respond-200-c3",
"name": "Respond 200 (HS)",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
240,
1300
],
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify({ received: true }) }}",
"options": {
"responseCode": 200
}
}
},
{
"id": "parse-hs-event",
"name": "Parse HS Event",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
480,
1300
],
"parameters": {
"jsCode": "// Purpose: Extract contact ID and changed tracked fields from HubSpot propertyChange webhook\n// Inputs: HubSpot webhook payload (array of change events for one contact)\n// Outputs: objectId, changes (map of propertyName->value), changeCount\n\nconst body = $input.first().json.body || $input.first().json;\nconst events = Array.isArray(body) ? body : [body];\n\nconst trackedProps = new Set(['firstname', 'lastname', 'company', 'phone', 'email']);\nconst changes = {};\nlet objectId = null;\n\nfor (const ev of events) {\n if (ev.objectId) objectId = String(ev.objectId);\n if (trackedProps.has(ev.propertyName)) {\n changes[ev.propertyName] = ev.propertyValue || '';\n }\n}\n\nconst changeCount = Object.keys(changes).length;\nreturn [{ json: { objectId, changes, changeCount } }];"
}
},
{
"id": "tracked-prop",
"name": "Tracked Property?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
720,
1300
],
"parameters": {
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "check-change-count",
"leftValue": "={{ $json.changeCount }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "gt"
}
}
]
},
"options": {}
}
},
{
"id": "get-hs-full",
"name": "Get Full HS Contact",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
960,
1300
],
"parameters": {
"method": "GET",
"url": "=https://api.hubapi.com/crm/v3/objects/contacts/{{ $json.objectId }}?properties=firstname,lastname,company,phone,mobilephone,email",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"options": {}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"onError": "continueRegularOutput"
},
{
"id": "has-phone-c3",
"name": "Has Phone? (HS)",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1200,
1300
],
"parameters": {
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "check-hs-phone-c3",
"leftValue": "={{ ($json.properties && ($json.properties.phone || $json.properties.mobilephone)) || '' }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
]
},
"options": {}
}
},
{
"id": "normalize-phone-c3",
"name": "Normalize Phone (HS)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1440,
1300
],
"parameters": {
"jsCode": "// Purpose: Normalize HubSpot contact phone for Quo lookup; carry all sync fields forward\n// Inputs: Get Full HS Contact response\n// Outputs: hsContactId, firstName, lastName, company, email, phone, phoneNormalized, phoneFormatted, changedFields\n\nconst hs = $input.first().json;\nconst props = hs.properties || {};\n\nconst raw = props.phone || props.mobilephone || '';\nconst digits = raw.replace(/[^0-9]/g, '');\nconst phoneNormalized = (digits.length === 11 && digits.startsWith('1')) ? digits.slice(1) : digits;\nconst phoneFormatted = phoneNormalized.length === 10\n ? '+1 (' + phoneNormalized.slice(0,3) + ') ' + phoneNormalized.slice(3,6) + '-' + phoneNormalized.slice(6)\n : raw;\n\nreturn [{ json: {\n hsContactId: hs.id,\n firstName: props.firstname || '',\n lastName: props.lastname || '',\n company: props.company || '',\n email: props.email || '',\n phone: raw,\n phoneNormalized,\n phoneFormatted,\n changedFields: $('Parse HS Event').first().json.changes\n} }];"
}
},
{
"id": "fetch-quo-c3",
"name": "Fetch Quo Contact (HS Change)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1680,
1300
],
"parameters": {
"jsCode": "// Purpose: Find Quo contact by phone via pagination for HS->Quo sync\n// Inputs: Normalize Phone (HS) with phoneNormalized and all contact fields\n// Outputs: all HS fields carried forward, quoContact object, matchCount\n\nconst normData = $input.first().json;\nconst searchBase = normData.phoneNormalized || '';\n\nconst toBase = (p) => {\n const d = (p || '').replace(/[^0-9]/g, '');\n return (d.length === 11 && d.startsWith('1')) ? d.slice(1) : d;\n};\n\nconst QUO_API_KEY = 'PMdBWaig1oFVhOBinL42xxAdKlqxfuSX';\nconst MAX_PAGES = 60;\nlet pageToken = null;\nlet matched = null;\n\nfor (let i = 0; i < MAX_PAGES; i++) {\n let url = 'https://api.openphone.com/v1/contacts?maxResults=50';\n if (pageToken) url += '&pageToken=' + encodeURIComponent(pageToken);\n let response;\n try {\n response = await this.helpers.httpRequest({\n method: 'GET', url,\n headers: { 'Authorization': QUO_API_KEY },\n json: true\n });\n } catch (err) { break; }\n const contacts = Array.isArray(response && response.data) ? response.data : [];\n for (const c of contacts) {\n const phones = Array.isArray(c && c.defaultFields && c.defaultFields.phoneNumbers) ? c.defaultFields.phoneNumbers : [];\n if (phones.some(p => toBase(p.value) === searchBase)) { matched = c; break; }\n }\n if (matched) break;\n pageToken = response && response.nextPageToken;\n if (!pageToken) break;\n}\n\nreturn [{ json: { ...normData, quoContact: matched, matchCount: matched ? 1 : 0 } }];"
},
"onError": "continueRegularOutput"
},
{
"id": "quo-found-c3",
"name": "Quo Contact Found?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1920,
1300
],
"parameters": {
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "check-quo-match",
"leftValue": "={{ $json.matchCount }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "gt"
}
}
]
},
"options": {}
}
},
{
"id": "build-quo-patch",
"name": "Build Quo Patch",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2160,
1160
],
"parameters": {
"jsCode": "// Purpose: Compare HS changed fields with current Quo contact; build minimal PUT body\n// Inputs: Fetch Quo Contact (HS Change) with quoContact and changedFields\n// Outputs: quoContactId, putBody (full merged defaultFields), changeCount\n\nconst data = $input.first().json;\nconst quoContact = data.quoContact || {};\nconst quoFields = quoContact.defaultFields || {};\nconst changedFields = data.changedFields || {};\n\nconst toBase = (p) => { const d = (p||'').replace(/[^0-9]/g,''); return (d.length===11&&d.startsWith('1'))?d.slice(1):d; };\nconst quoPhoneBase = toBase(((quoFields.phoneNumbers || [])[0] || {}).value || '');\nconst quoEmailVal = ((quoFields.emails || [])[0] || {}).value || '';\n\nconst mergedFields = { ...quoFields };\nlet changeCount = 0;\n\nif ('firstname' in changedFields && (quoFields.firstName || '') !== changedFields.firstname) {\n mergedFields.firstName = changedFields.firstname; changeCount++;\n}\nif ('lastname' in changedFields && (quoFields.lastName || '') !== changedFields.lastname) {\n mergedFields.lastName = changedFields.lastname; changeCount++;\n}\nif ('company' in changedFields && (quoFields.company || '') !== changedFields.company) {\n mergedFields.company = changedFields.company; changeCount++;\n}\nif ('phone' in changedFields && toBase(changedFields.phone) !== quoPhoneBase) {\n mergedFields.phoneNumbers = [{ value: changedFields.phone }]; changeCount++;\n}\nif ('email' in changedFields && quoEmailVal !== changedFields.email) {\n mergedFields.emails = [{ value: changedFields.email }]; changeCount++;\n}\n\nreturn [{ json: { quoContactId: quoContact.id, putBody: { defaultFields: mergedFields }, changeCount } }];"
}
},
{
"id": "has-changes-c3",
"name": "Has Changes? (HS->Quo)",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
2400,
1160
],
"parameters": {
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "check-changes-c3",
"leftValue": "={{ $json.changeCount }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "gt"
}
}
]
},
"options": {}
}
},
{
"id": "patch-quo-c3",
"name": "Patch Quo Contact",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2640,
1160
],
"parameters": {
"jsCode": "// Purpose: Update existing Quo contact via OpenPhone API (PUT with merged fields)\n// Inputs: Build Quo Patch with quoContactId and putBody\n// Outputs: success confirmation\n\nconst data = $input.first().json;\nconst QUO_API_KEY = 'PMdBWaig1oFVhOBinL42xxAdKlqxfuSX';\n\nawait this.helpers.httpRequest({\n method: 'PUT',\n url: `https://api.openphone.com/v1/contacts/${data.quoContactId}`,\n headers: { 'Authorization': QUO_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify(data.putBody),\n json: false\n});\n\nreturn [{ json: { success: true, quoContactId: data.quoContactId } }];"
},
"onError": "continueRegularOutput"
},
{
"id": "create-quo-c3",
"name": "Create Quo Contact",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2160,
1440
],
"parameters": {
"jsCode": "// Purpose: Create new Quo contact from HubSpot data when no Quo match exists\n// Inputs: Fetch Quo Contact (HS Change) with HS field values\n// Outputs: success confirmation\n\nconst data = $input.first().json;\nconst QUO_API_KEY = 'PMdBWaig1oFVhOBinL42xxAdKlqxfuSX';\n\nconst defaultFields = {};\nif (data.firstName) defaultFields.firstName = data.firstName;\nif (data.lastName) defaultFields.lastName = data.lastName;\nif (data.company) defaultFields.company = data.company;\nif (data.phoneFormatted) defaultFields.phoneNumbers = [{ value: data.phoneFormatted }];\nif (data.email) defaultFields.emails = [{ value: data.email }];\n\nawait this.helpers.httpRequest({\n method: 'POST',\n url: 'https://api.openphone.com/v1/contacts',\n headers: { 'Authorization': QUO_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ defaultFields }),\n json: false\n});\n\nreturn [{ json: { success: true, created: true } }];"
},
"onError": "continueRegularOutput"
},
{
"id": "has-company-c2",
"name": "Has Company? (C2)",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
2400,
560
],
"parameters": {
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "check-company-c2",
"leftValue": "={{ $('Normalize Phone (Contact)').first().json.company }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
]
},
"options": {}
}
},
{
"id": "search-hs-company-c2",
"name": "Search HS Company (C2)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2640,
420
],
"parameters": {
"method": "POST",
"url": "https://api.hubapi.com/crm/v3/objects/companies/search",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ filterGroups: [{ filters: [{ propertyName: 'name', operator: 'EQ', value: $('Normalize Phone (Contact)').first().json.company }] }], properties: ['name'], limit: 1 }) }}",
"options": {}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"onError": "continueRegularOutput"
},
{
"id": "company-exists-c2",
"name": "Company Exists? (C2)",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
2880,
420
],
"parameters": {
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "check-company-total-c2",
"leftValue": "={{ $json.total }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "gt"
}
}
]
},
"options": {}
}
},
{
"id": "set-company-id-c2",
"name": "Set Company ID (C2)",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
3120,
300
],
"parameters": {
"assignments": {
"assignments": [
{
"id": "1",
"name": "hsCompanyId",
"value": "={{ $json.results[0].id }}",
"type": "string"
}
]
},
"options": {}
}
},
{
"id": "create-hs-company-c2",
"name": "Create HS Company (C2)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
3120,
540
],
"parameters": {
"method": "POST",
"url": "https://api.hubapi.com/crm/v3/objects/companies",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ properties: { name: $('Normalize Phone (Contact)').first().json.company } }) }}",
"options": {}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"onError": "continueRegularOutput"
},
{
"id": "set-new-company-id-c2",
"name": "Set New Company ID (C2)",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
3360,
540
],
"parameters": {
"assignments": {
"assignments": [
{
"id": "1",
"name": "hsCompanyId",
"value": "={{ $json.id }}",
"type": "string"
}
]
},
"options": {}
}
},
{
"id": "get-all-ids-c2",
"name": "Get All IDs (C2)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3600,
420
],
"parameters": {
"jsCode": "// Purpose: Collect hsContactId + hsCompanyId for association in Chain 2\n// Inputs: Set Company ID (C2) or Set New Company ID (C2); Build HS Patch for contact ID\n// Outputs: hsContactId, hsCompanyId\n\nconst hsCompanyId = $input.first().json.hsCompanyId;\nconst hsContactId = $('Build HS Patch').first().json.hsContactId;\n\nif (!hsContactId || !hsCompanyId) {\n throw new Error('Cannot associate - missing IDs. contactId=' + hsContactId + ', companyId=' + hsCompanyId);\n}\n\nreturn [{ json: { hsContactId, hsCompanyId } }];"
}
},
{
"id": "associate-c2",
"name": "Associate Contact + Company (C2)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
3840,
420
],
"parameters": {
"method": "PUT",
"url": "=https://api.hubapi.com/crm/v3/objects/contacts/{{ $json.hsContactId }}/associations/companies/{{ $json.hsCompanyId }}/contact_to_company",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"options": {}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"onError": "continueRegularOutput"
},
{
"id": "get-old-assoc-c2",
"name": "Get Old Company Associations",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2500,
700
],
"parameters": {
"method": "GET",
"url": "=https://api.hubapi.com/crm/v3/objects/contacts/{{ $('Build HS Patch').first().json.hsContactId }}/associations/companies",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"options": {}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"onError": "continueRegularOutput"
},
{
"id": "remove-old-assoc-c2",
"name": "Remove Old Company Associations",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2780,
700
],
"parameters": {
"method": "POST",
"url": "https://api.hubapi.com/crm/v4/associations/contacts/companies/batch/archive",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ inputs: ($json.results || []).map(r => ({ from: { id: String($('Build HS Patch').first().json.hsContactId) }, to: [{ id: String(r.id) }] })) }) }}",
"options": {}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"onError": "continueRegularOutput"
},
{
"id": "fetch-call-details",
"name": "Fetch Call Details",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2160,
280
],
"parameters": {
"jsCode": "// Purpose: Fetch call summary/transcript from OpenPhone for AI prospect classification\n// Inputs: Parse Quo Payload (callId)\n// Outputs: callId, summary, hasSummary\n\nconst QUO_API_KEY = 'PMdBWaig1oFVhOBinL42xxAdKlqxfuSX';\nconst callId = $('Parse Quo Payload').first().json.callId;\n\nif (!callId) return [{ json: { callId: null, summary: '', hasSummary: false } }];\n\nlet text = '';\ntry {\n const response = await this.helpers.httpRequest({\n method: 'GET',\n url: `https://api.openphone.com/v1/calls/${callId}`,\n headers: { 'Authorization': QUO_API_KEY },\n json: true\n });\n const data = response.data || {};\n const summary = data.summary || '';\n const dialogue = (data.transcription && Array.isArray(data.transcription.dialogue))\n ? data.transcription.dialogue.map(d => `${d.speaker}: ${d.text}`).join(' ')\n : '';\n text = summary || dialogue;\n} catch (err) {}\n\nreturn [{ json: { callId, summary: text, hasSummary: !!text } }];"
},
"onError": "continueRegularOutput"
},
{
"id": "has-summary",
"name": "Has Call Summary?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
2400,
280
],
"parameters": {
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "check-summary",
"leftValue": "={{ $json.hasSummary }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
]
},
"options": {}
}
},
{
"id": "ai-classify-caller",
"name": "AI Classify Caller",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2640,
280
],
"parameters": {
"method": "POST",
"url": "https://api.openai.com/v1/chat/completions",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "openAiApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: 'gpt-4o-mini', messages: [{ role: 'system', content: 'You are a sales qualifier for Renellence, a Canadian medical device company selling to healthcare facilities, clinics, and medical professionals. Analyze the call summary and determine if this caller is a potential business prospect \u2014 a client, healthcare professional, partner, or referral source. Answer YES if there is any business relevance to a medical device company. Answer NO if the caller is clearly unrelated \u2014 e.g. personal call, wrong number, spam, construction worker, delivery driver, or other non-medical tradesperson. Respond with only YES or NO, nothing else.' }, { role: 'user', content: 'Call summary: ' + $json.summary }], max_tokens: 5 }) }}",
"options": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"onError": "continueRegularOutput"
},
{
"id": "is-prospect",
"name": "Is Prospect?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
2880,
280
],
"parameters": {
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "check-prospect",
"leftValue": "={{ ($json.choices && $json.choices[0] && $json.choices[0].message && $json.choices[0].message.content) || '' }}",
"rightValue": "YES",
"operator": {
"type": "string",
"operation": "contains"
}
}
]
},
"options": {}
}
},
{
"id": "create-hs-contact",
"name": "Create HS Contact",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
3120,
280
],
"parameters": {
"method": "POST",
"url": "https://api.hubapi.com/crm/v3/objects/contacts",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ (() => { const d = $('Normalize Contact Data').first().json; const props = { phone: d.phoneFormatted }; if (d.firstName) props.firstname = d.firstName; if (d.lastName) props.lastname = d.lastName; return JSON.stringify({ properties: props }); })() }}",
"options": {}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"onError": "continueRegularOutput"
},
{
"id": "set-new-contact-id",
"name": "Set New Contact ID",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
3360,
280
],
"parameters": {
"assignments": {
"assignments": [
{
"id": "1",
"name": "hsContactId",
"value": "={{ $json.id }}",
"type": "string"
}
]
},
"options": {}
}
}
],
"connections": {
"Quo Call Webhook": {
"main": [
[
{
"node": "Respond 200",
"type": "main",
"index": 0
}
]
]
},
"Respond 200": {
"main": [
[
{
"node": "Parse Quo Payload",
"type": "main",
"index": 0
}
]
]
},
"Parse Quo Payload": {
"main": [
[
{
"node": "Has Phone?",
"type": "main",
"index": 0
}
]
]
},
"Has Phone?": {
"main": [
[
{
"node": "Normalize Phone",
"type": "main",
"index": 0
}
]
]
},
"Normalize Phone": {
"main": [
[
{
"node": "Fetch Quo Contact",
"type": "main",
"index": 0
}
]
]
},
"Fetch Quo Contact": {
"main": [
[
{
"node": "Normalize Contact Data",
"type": "main",
"index": 0
}
]
]
},
"Normalize Contact Data": {
"main": [
[
{
"node": "Search HS by Phone",
"type": "main",
"index": 0
}
]
]
},
"Search HS by Phone": {
"main": [
[
{
"node": "Contact Exists?",
"type": "main",
"index": 0
}
]
]
},
"Contact Exists?": {
"main": [
[
{
"node": "Update HS Contact",
"type": "main",
"index": 0
}
],
[
{
"node": "Fetch Call Details",
"type": "main",
"index": 0
}
]
]
},
"Update HS Contact": {
"main": [
[
{
"node": "Set Contact ID",
"type": "main",
"index": 0
}
]
]
},
"Set Contact ID": {
"main": [
[
{
"node": "Has Company?",
"type": "main",
"index": 0
}
]
]
},
"Has Company?": {
"main": [
[
{
"node": "Search HS Company",
"type": "main",
"index": 0
}
]
]
},
"Search HS Company": {
"main": [
[
{
"node": "Company Exists?",
"type": "main",
"index": 0
}
]
]
},
"Company Exists?": {
"main": [
[
{
"node": "Set Company ID",
"type": "main",
"index": 0
}
],
[
{
"node": "Create HS Company",
"type": "main",
"index": 0
}
]
]
},
"Set Company ID": {
"main": [
[
{
"node": "Get All IDs",
"type": "main",
"index": 0
}
]
]
},
"Create HS Company": {
"main": [
[
{
"node": "Set New Company ID",
"type": "main",
"index": 0
}
]
]
},
"Set New Company ID": {
"main": [
[
{
"node": "Get All IDs",
"type": "main",
"index": 0
}
]
]
},
"Get All IDs": {
"main": [
[
{
"node": "Associate Contact + Company",
"type": "main",
"index": 0
}
]
]
},
"Quo Contact Updated Webhook": {
"main": [
[
{
"node": "Respond 200 (Contact)",
"type": "main",
"index": 0
}
]
]
},
"Respond 200 (Contact)": {
"main": [
[
{
"node": "Parse Quo Contact Update",
"type": "main",
"index": 0
}
]
]
},
"Parse Quo Contact Update": {
"main": [
[
{
"node": "Has Phone? (Contact)",
"type": "main",
"index": 0
}
]
]
},
"Has Phone? (Contact)": {
"main": [
[
{
"node": "Normalize Phone (Contact)",
"type": "main",
"index": 0
}
]
]
},
"Normalize Phone (Contact)": {
"main": [
[
{
"node": "Search HS by Phone (Contact)",
"type": "main",
"index": 0
}
]
]
},
"Search HS by Phone (Contact)": {
"main": [
[
{
"node": "HS Contact Found?",
"type": "main",
"index": 0
}
]
]
},
"HS Contact Found?": {
"main": [
[
{
"node": "Build HS Patch",
"type": "main",
"index": 0
}
]
]
},
"Build HS Patch": {
"main": [
[
{
"node": "Has Changes? (Quo->HS)",
"type": "main",
"index": 0
}
]
]
},
"Has Changes? (Quo->HS)": {
"main": [
[
{
"node": "Patch HS Contact",
"type": "main",
"index": 0
}
]
]
},
"HS Contact Change Webhook": {
"main": [
[
{
"node": "Respond 200 (HS)",
"type": "main",
"index": 0
}
]
]
},
"Respond 200 (HS)": {
"main": [
[
{
"node": "Parse HS Event",
"type": "main",
"index": 0
}
]
]
},
"Parse HS Event": {
"main": [
[
{
"node": "Tracked Property?",
"type": "main",
"index": 0
}
]
]
},
"Tracked Property?": {
"main": [
[
{
"node": "Get Full HS Contact",
"type": "main",
"index": 0
}
]
]
},
"Get Full HS Contact": {
"main": [
[
{
"node": "Has Phone? (HS)",
"type": "main",
"index": 0
}
]
]
},
"Has Phone? (HS)": {
"main": [
[
{
"node": "Normalize Phone (HS)",
"type": "main",
"index": 0
}
]
]
},
"Normalize Phone (HS)": {
"main": [
[
{
"node": "Fetch Quo Contact (HS Change)",
"type": "main",
"index": 0
}
]
]
},
"Fetch Quo Contact (HS Change)": {
"main": [
[
{
"node": "Quo Contact Found?",
"type": "main",
"index": 0
}
]
]
},
"Quo Contact Found?": {
"main": [
[
{
"node": "Build Quo Patch",
"type": "main",
"index": 0
}
],
[
{
"node": "Create Quo Contact",
"type": "main",
"index": 0
}
]
]
},
"Bu
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
httpHeaderAuthopenAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
[QUO] Bidirectional Contact Sync. Uses httpRequest. Webhook trigger; 58 nodes.
Source: https://github.com/Info-Renellence/hubspot-automation/blob/d011d13e97d0cbb6515ceeb4f8909144348c8e95/workflows/workflow-live.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This n8n template provides enterprise-level version control for your workflows using GitHub integration. Stop losing hours to broken workflows and manual exports – get proper commit history, visual di
This flow creates dummy files for every item added in your *Arrs (Radarr/Sonarr) with the tag .
This workflow receives webhook requests from a content calendar and uses the X API v2 to publish text posts, threads, image/video posts, and polls, as well as delete existing posts and run a credentia
This workflow acts as a central API gateway for all technical indicator agents in the Binance Spot Market Quant AI system. It listens for incoming webhook requests and dynamically routes them to the c
Sign PDF documents with legally-compliant digital signatures using X.509 certificates. Supports multiple PAdES signature levels (B, T, LT, LTA) with optional visible stamps.