This workflow corresponds to n8n.io template #16832 — we link there as the canonical source.
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 →
{
"id": "8F9tUMFGHDXlX3fN",
"name": "Factor timeline to SFDC test",
"tags": [],
"nodes": [
{
"id": "0813aef7-a5eb-4252-a5a3-7a82503acf90",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-3904,
-528
],
"parameters": {
"width": 480,
"height": 880,
"content": "## Factor timeline to SFDC test\n\n### How it works\n\nThis workflow runs on a schedule, tracks the last successful run time, and retrieves recently qualified Salesforce leads. It processes leads one at a time, checks whether each lead has an email address, enriches valid-email leads through the Factors.ai account API, and writes the appropriate result back to Salesforce. After all leads are processed successfully, it commits the new lastRunTime so the next scheduled run only picks up new changes.\n\n### Setup steps\n\n- Configure the Schedule Trigger with the desired polling frequency.\n- Connect Salesforce credentials and verify the lead query in \u201cGet SF Leads MQL\u201d plus the update mappings in all Salesforce update nodes.\n- Configure the Factors.ai API endpoint, authentication, and account parameters in the HTTP Request node.\n- Review the static-data logic in \u201cInit Run\u201d and \u201cFinalize Run\u201d to ensure lastRunTime is stored and advanced correctly for your environment.\n\n### Customization\n\nYou can adjust the Salesforce MQL lead criteria, the wait durations used for API/Salesforce rate limiting, and the Salesforce fields updated for no-email, no-Factors-email, and successful enrichment outcomes."
},
"typeVersion": 1
},
{
"id": "b76464be-a045-4d06-917d-682adeb26a4d",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-3344,
48
],
"parameters": {
"color": 7,
"width": 640,
"height": 304,
"content": "## Schedule and fetch leads\n\nStarts the workflow on a schedule, initializes the run window from stored state, and retrieves eligible MQL leads from Salesforce."
},
"typeVersion": 1
},
{
"id": "34e5d534-24ab-4a5f-9c03-86c0645daf26",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2896,
-432
],
"parameters": {
"color": 7,
"width": 640,
"height": 432,
"content": "## Batch and email gate\n\nControls item-by-item processing, routes completed runs to finalization, and checks each lead for whether it has an email address before enrichment."
},
"typeVersion": 1
},
{
"id": "967bde5c-1818-493e-b6dd-87d10f86615c",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2224,
-512
],
"parameters": {
"color": 7,
"width": 240,
"height": 608,
"content": "## Email availability handling\n\nHandles the two nearby email-availability outcomes: extracts the email domain for leads with an email, or marks leads without an email back in Salesforce."
},
"typeVersion": 1
},
{
"id": "06e76797-d84c-4f00-9b9c-6f5be80e2e05",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1952,
-416
],
"parameters": {
"color": 7,
"width": 640,
"height": 320,
"content": "## Call Factors API\n\nWaits before the external request, calls the Factors.ai account endpoint using the extracted domain, and runs custom JavaScript to normalize or prepare the API response."
},
"typeVersion": 1
},
{
"id": "eaa90374-fae7-46d7-aa12-00a15cd123bf",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1280,
-416
],
"parameters": {
"color": 7,
"width": 416,
"height": 320,
"content": "## Evaluate Factors email\n\nInspects the processed Factors.ai response and branches based on whether a matching email was found for the lead/account."
},
"typeVersion": 1
},
{
"id": "4104d6a6-edf3-4662-ab0d-e498ca26510b",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
-832,
-528
],
"parameters": {
"color": 7,
"width": 496,
"height": 672,
"content": "## Update Salesforce outcomes\n\nApplies the final Salesforce updates for the Factors lookup result, including rate-limiting before a successful lead update and a separate update path when no Factors email is found."
},
"typeVersion": 1
},
{
"id": "09bc44c1-7362-439a-b7a2-a82b46cf905d",
"name": "When Scheduled Time Reached",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-3296,
176
],
"parameters": {
"rule": {
"interval": [
{}
]
}
},
"typeVersion": 1.3
},
{
"id": "1e29ebcb-e2a9-4cc9-b962-78aa21940eed",
"name": "Calculate Dynamic Last Run Time",
"type": "n8n-nodes-base.code",
"position": [
-3072,
176
],
"parameters": {
"jsCode": "// Dynamic lastRunTime via static data instead of hardcoded date\nconst staticData = $getWorkflowStaticData('global');\nconst now = new Date().toISOString();\n\n// First run default: 7 days ago to avoid processing entire historical backlog\nconst fallback = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();\nconst lastRunTime = staticData.lastRunTime || fallback;\n\n// Store as pendingRunTime - only committed to lastRunTime after successful completion\n// This prevents missing leads if the workflow fails mid-run\nstaticData.pendingRunTime = now;\n\nreturn [{\n json: {\n lastRunTime,\n currentRunTime: now\n }\n}];"
},
"typeVersion": 2
},
{
"id": "ef00cc09-a207-4f38-9459-c13673611569",
"name": "Fetch SF MQL Leads",
"type": "n8n-nodes-base.salesforce",
"position": [
-2848,
176
],
"parameters": {
"options": {
"conditionsUi": {
"conditionValues": [
{
"field": "CreatedDate",
"value": "={{ $('Calculate Dynamic Last Run Time').item.json.lastRunTime }}",
"operation": ">="
},
{
"field": "Conversion_type__c",
"value": "Demo Request"
},
{
"field": "IsConverted",
"value": "={{ false }}"
},
{
"field": "Factors_Synced__c",
"value": "={{ false }}"
}
]
}
},
"operation": "getAll",
"returnAll": true
},
"credentials": {
"salesforceOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "ece2c85f-f912-42bd-b6a4-8602e9cceda4",
"name": "Loop Over Lead Batches",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-2848,
-224
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "36ef41cc-8b8e-4b7b-8466-a6cc27abd937",
"name": "If Email Exists",
"type": "n8n-nodes-base.if",
"position": [
-2400,
-160
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "e2e1052e-8f77-4c22-8836-d7a43da6e2ea",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json.Email }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "aa51d2a1-3e6b-4ddf-9a13-06f8854b8df4",
"name": "Extract Domain from Email",
"type": "n8n-nodes-base.code",
"position": [
-2176,
-256
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Extracts domain AND preserves all original SF lead fields for downstream reference\nconst email = $json.Email?.toLowerCase() || '';\nconst domain = email.includes('@') ? email.split('@')[1] : null;\n\nreturn {\n json: {\n ...$json,\n account_domain: domain\n }\n};"
},
"typeVersion": 2
},
{
"id": "a5cf4280-e0f5-4e40-b9b2-547fb3b293b4",
"name": "Wait 2 Seconds",
"type": "n8n-nodes-base.wait",
"position": [
-1904,
-256
],
"parameters": {
"amount": 2
},
"typeVersion": 1.1
},
{
"id": "5d59dc00-c653-4c44-8cf5-5405eb7763f9",
"name": "Fetch Journey Data from API",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"position": [
-1680,
-256
],
"parameters": {
"url": "=https://api.factors.ai/open/v1/account/{{$json.account_domain}}/journey",
"options": {
"timeout": 10000
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "Bearer YOUR_TOKEN_HERE"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.4,
"alwaysOutputData": true
},
{
"id": "ea9afa64-0c58-4281-bb8e-2bb4ab186869",
"name": "Tidy API Response Data",
"type": "n8n-nodes-base.code",
"position": [
-1456,
-256
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const tidyJson = (data) => {\n if (typeof data === 'string') {\n try { return JSON.parse(data); } catch (e) { return {}; }\n }\n return data || {};\n};\n\nconst user = $input.item.json;\n\nreturn {\n user_id: user.user_id,\n user_name: user.user_name,\n is_anonymous: user.is_anonymous,\n user_properties: tidyJson(user.user_properties),\n activities_count: user.user_activities ? user.user_activities.length : 0,\n activities: (user.user_activities || []).map(activity => ({\n name: activity.display_name,\n time: activity.timestamp ? new Date(activity.timestamp * 1000).toISOString() : null,\n props: tidyJson(activity.properties)\n })),\n last_event: user.user_last_event_at\n};"
},
"typeVersion": 2
},
{
"id": "832e98b3-0e9a-4bb4-862f-2a3e6cdd6d6d",
"name": "Locate Email in Data",
"type": "n8n-nodes-base.code",
"position": [
-1232,
-256
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "function findEmail(obj) {\n if (!obj) return null;\n if (typeof obj === 'string') {\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (emailRegex.test(obj)) return obj;\n }\n if (typeof obj === 'object') {\n for (const key in obj) {\n const result = findEmail(obj[key]);\n if (result) return result;\n }\n }\n return null;\n}\n\nconst email = findEmail($json);\nconst emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nconst isValid = email ? emailRegex.test(email) : false;\n\nreturn {\n json: {\n ...$json,\n emailFound: email,\n validEmail: isValid\n }\n};"
},
"typeVersion": 2
},
{
"id": "7261cc25-7506-4198-a109-b8a4845d11c6",
"name": "If Valid Email Found",
"type": "n8n-nodes-base.if",
"position": [
-1008,
-256
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "f07a5a87-d50d-4246-b32d-e65fb9a6a147",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $json.validEmail }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.3
},
{
"id": "be2d64db-f278-4cca-a878-fd399c7e2290",
"name": "Wait 1 Second for Update",
"type": "n8n-nodes-base.wait",
"position": [
-784,
-352
],
"parameters": {
"amount": 1
},
"typeVersion": 1.1
},
{
"id": "bace03a9-dc0d-44a6-9254-e6d8701c7a66",
"name": "Update Lead with Data",
"type": "n8n-nodes-base.salesforce",
"position": [
-480,
-16
],
"parameters": {
"leadId": "={{ $('Extract Domain from Email').item.json.Id }}",
"operation": "update",
"updateFields": {
"customFieldsUi": {
"customFieldsValues": [
{
"value": "={{\n(() => {\n const MAX_CHARS = 30000;\n const items = $('Locate Email in Data').item.json.activities || [];\n const blocks = [];\n\n for (const a of items) {\n const lines = [];\n const name = a.name ?? '';\n const raw = a.time ?? '';\n const time = raw.replace('T', ' T ');\n lines.push(`\u2022 ${name} @ ${time}`);\n\n if (a.props) {\n const channel = a.props.$channel ?? a.props.channel;\n const pageUrl = a.props.$page_url ?? a.props.$referrer_url;\n const initialPage = a.props.$initial_page_url;\n const initialRef = a.props.$initial_referrer_url;\n const pageCount = a.props.$page_count ?? a.props.page_count;\n const sessionTime = a.props.$session_spent_time ?? a.props.session_spent_time;\n\n if (channel) lines.push(` - Channel: ${channel}`);\n if (initialPage) lines.push(` - Initial Page: ${initialPage}`);\n if (initialRef) lines.push(` - Initial Referrer: ${initialRef}`);\n if (pageUrl) lines.push(` - Last URL: ${pageUrl}`);\n if (pageCount !== undefined && pageCount !== '')\n lines.push(` - Page Count: ${pageCount}`);\n if (sessionTime !== undefined && sessionTime !== '')\n lines.push(` - Session Spent (s): ${sessionTime}`);\n }\n blocks.push(lines.join('\\n'));\n }\n\n blocks.reverse();\n let output = '';\n for (const block of blocks) {\n if ((output + block + '\\n\\n').length > MAX_CHARS) break;\n output += block + '\\n\\n';\n }\n return output.trim();\n})()\n}}\n",
"fieldId": "Factor_Lead_Journey_Summary__c"
},
{
"value": "={{ true }}",
"fieldId": "Factors_Synced__c"
}
]
}
}
},
"credentials": {
"salesforceOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "f51bb419-8ee8-47d2-83f7-83439db1ed01",
"name": "Update Lead Missing Email",
"type": "n8n-nodes-base.salesforce",
"position": [
-2176,
-64
],
"parameters": {
"leadId": "={{ $json.Id }}",
"operation": "update",
"updateFields": {
"customFieldsUi": {
"customFieldsValues": [
{
"value": "No Email in SF Lead",
"fieldId": "Factors_Status__c"
}
]
}
}
},
"credentials": {
"salesforceOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "e958076b-c55b-4606-9343-97e63c1a786d",
"name": "Update Lead No Journey Email",
"type": "n8n-nodes-base.salesforce",
"position": [
-784,
-160
],
"parameters": {
"leadId": "={{ $('Extract Domain from Email').item.json.Id }}",
"operation": "update",
"updateFields": {
"customFieldsUi": {
"customFieldsValues": [
{
"value": "No Email Match in Factors",
"fieldId": "Factors_Status__c"
}
]
}
}
},
"credentials": {
"salesforceOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "f974fdf3-6339-47a8-be92-dbc1fa9cb082",
"name": "Commit Last Run Time Update",
"type": "n8n-nodes-base.code",
"position": [
-2576,
-272
],
"parameters": {
"jsCode": "// Commit lastRunTime only after all leads have been successfully processed\nconst staticData = $getWorkflowStaticData('global');\nif (staticData.pendingRunTime) {\n staticData.lastRunTime = staticData.pendingRunTime;\n delete staticData.pendingRunTime;\n}\nreturn [{ json: { status: 'Workflow completed successfully', lastRunTime: staticData.lastRunTime } }];"
},
"typeVersion": 2
}
],
"active": true,
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
},
"versionId": "6470c059-6f20-4742-aaf9-8452f72d7164",
"nodeGroups": [],
"connections": {
"Wait 2 Seconds": {
"main": [
[
{
"node": "Fetch Journey Data from API",
"type": "main",
"index": 0
}
]
]
},
"If Email Exists": {
"main": [
[
{
"node": "Extract Domain from Email",
"type": "main",
"index": 0
}
],
[
{
"node": "Update Lead Missing Email",
"type": "main",
"index": 0
}
]
]
},
"Fetch SF MQL Leads": {
"main": [
[
{
"node": "Loop Over Lead Batches",
"type": "main",
"index": 0
}
]
]
},
"If Valid Email Found": {
"main": [
[
{
"node": "Wait 1 Second for Update",
"type": "main",
"index": 0
}
],
[
{
"node": "Update Lead No Journey Email",
"type": "main",
"index": 0
}
]
]
},
"Locate Email in Data": {
"main": [
[
{
"node": "If Valid Email Found",
"type": "main",
"index": 0
}
]
]
},
"Update Lead with Data": {
"main": [
[
{
"node": "Loop Over Lead Batches",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Lead Batches": {
"main": [
[
{
"node": "Commit Last Run Time Update",
"type": "main",
"index": 0
}
],
[
{
"node": "If Email Exists",
"type": "main",
"index": 0
}
]
]
},
"Tidy API Response Data": {
"main": [
[
{
"node": "Locate Email in Data",
"type": "main",
"index": 0
}
]
]
},
"Wait 1 Second for Update": {
"main": [
[
{
"node": "Update Lead with Data",
"type": "main",
"index": 0
}
]
]
},
"Extract Domain from Email": {
"main": [
[
{
"node": "Wait 2 Seconds",
"type": "main",
"index": 0
}
]
]
},
"Update Lead Missing Email": {
"main": [
[
{
"node": "Loop Over Lead Batches",
"type": "main",
"index": 0
}
]
]
},
"Fetch Journey Data from API": {
"main": [
[
{
"node": "Tidy API Response Data",
"type": "main",
"index": 0
}
]
]
},
"When Scheduled Time Reached": {
"main": [
[
{
"node": "Calculate Dynamic Last Run Time",
"type": "main",
"index": 0
}
]
]
},
"Update Lead No Journey Email": {
"main": [
[
{
"node": "Loop Over Lead Batches",
"type": "main",
"index": 0
}
]
]
},
"Calculate Dynamic Last Run Time": {
"main": [
[
{
"node": "Fetch SF MQL Leads",
"type": "main",
"index": 0
}
]
]
}
}
}
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.
salesforceOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow runs on a schedule to fetch new, unsynced Salesforce leads, pulls account journey activity from the Factors.ai API based on the lead’s email domain, and updates the lead in Salesforce with a formatted journey summary and sync/status fields. Runs on a schedule and…
Source: https://n8n.io/workflows/16832/ — 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.
LinkedIn Lead Gen - Sync to CRM. Uses httpRequest, salesforce, slack. Scheduled trigger; 5 nodes.
*Smartlead to HubSpot Performance Analytics A streamlined workflow to analyze your Smartlead performance metrics by tracking lifecycle stages in HubSpot and generating automated reports.*
This workflow automatically captures website visitors using Albacross, enriches their data with Datagma, and syncs it into HubSpot. Once leads are created or updated, a personalized email is generated
This workflow automatically checks Zoho CRM every 5 minutes for newly created leads, enriches each lead using People Data Labs, evaluates its quality using Google Gemini (LLM Chain) and updates the le
AI Lead Qualifier. Uses httpRequest, openAi, salesforce, slack. Webhook trigger; 8 nodes.