Recipe 02, Scheduled data sync (HubSpot → Salesforce)
Goal: every hour, fetch HubSpot contacts created in the last hour and upsert them as Salesforce leads.
Difficulty: Intermediate · Time: ~25 minutes · Connectors: HubSpot, Salesforce
Prerequisites
- HubSpot credential with read access to Contacts.
- Salesforce credential with write access to Leads.
- A test contact in HubSpot you can use to verify.
Finished workflow
┌──────────┐ ┌─────────────────────┐ ┌──────┐ ┌───────────┐ ┌────────────────────┐
│ Schedule │──▶│ HubSpot: list │──▶│ Loop │──▶│ Transform │──▶│ Salesforce: upsert │
└──────────┘ │ contacts since │ └──┬───┘ └───────────┘ │ Lead │
│ last sync │ │ done └────────────────────┘
└─────────────────────┘ ▼
┌─────────┐
│ Set: │
│ lastSync│
└─────────┘Step-by-step
1. Create the workflow
/workflows → Create workflow → HubSpot → Salesforce hourly sync.
2. Configure the schedule
Replace Start with a Schedule Trigger:
- Type:
Cron - Expression:
0 * * * *(top of every hour) - Time zone: your workspace's
3. List HubSpot contacts since last sync
Add HubSpot node:
- Credential: your HubSpot credential
- Operation:
List contacts - Filter:
createdAt > {{ $workflow.variables.lastSync }} - Sort:
createdAt asc - Limit: 100
For the first run, $workflow.variables.lastSync will be empty. Add a small "first-run" fallback:
Drag a Set node before the HubSpot node:
- Name:
defaultLastSync - Value:
{{ $workflow.variables.lastSync || $trigger.previousScheduledTime }} - Scope:
run
Then update the HubSpot filter to: createdAt > {{ $node["defaultLastSync"].json.value }}.
4. Loop over the results
Add a Loop node connected to HubSpot's output:
- Mode:
array - Source:
{{ $node["HubSpot"].json.results }}
5. Transform each contact
In the Loop's body, add a Transform node:
map:
Email: {{ $input.email }}
FirstName: {{ $input.firstName }}
LastName: {{ $input.lastName }}
Company: {{ $input.company }}
LeadSource: HubSpot6. Upsert as Salesforce lead
Add Salesforce node after Transform:
- Credential: Salesforce
- Operation:
Upsert Lead - External ID field:
Email(so duplicate emails don't create duplicate leads) - Fields: pull from Transform output
7. Remember the last sync time
After the Loop's done port, add a Set node:
- Name:
lastSync - Value:
{{ $trigger.scheduledTime }} - Scope:
workflow(persists across runs)
8. Save and activate
Save. Change status to Active. The first run happens at the next hour boundary.
Try it
Manually trigger the first run from the editor:
- Click Run in the toolbar.
- Watch the canvas, nodes turn green in sequence.
- Open Salesforce, verify the leads appear.
- Switch back; click into the execution detail.
- The
lastSyncvariable should now be populated. Next run will only fetch contacts created since.
Variations
Run only on weekdays
Cron: 0 * * * 1-5 (every hour, Mon–Fri).
Batch process with concurrency
Replace the Loop with a Distributed Execution node to run upserts in parallel across workers. Faster for large syncs.
Notify on failure
Add an Error Handling node around the Salesforce upsert. On error, route to a Slack node that posts to your ops channel.
Add a digest summary
After the Loop's done port, send an email summarising the run: count of contacts synced, count of failures.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| First run fetches no contacts | lastSync is empty; filter excludes all |
Use the defaultLastSync fallback as described |
| Duplicate leads in Salesforce | Not using upsert; or external ID field misconfigured | Switch operation to Upsert Lead with Email as the external ID |
| Salesforce rejects with "field required" | Lead has required fields beyond what HubSpot supplied | Add them in the Transform step with sensible defaults |
| Workflow times out on big batches | Default 5-min timeout | Workflow settings → Execution timeout: bump to 30 min, or switch to Distributed Execution |
Next
Found something out of date? This page lives in the Flero docs content set.