Recipe 02: Scheduled data sync (HubSpot → Postgres)
Goal: every hour, fetch the newest HubSpot leads and upsert them into a Postgres table, so reporting tools can query them.
Difficulty: Intermediate · Time: about 25 minutes · Connectors: HubSpot, Database
Prerequisites
- A HubSpot credential with read access to contacts.
- A Postgres database Flero can reach, with this table:
CREATE TABLE hubspot_leads ( hubspot_id text PRIMARY KEY, email text, first_name text, last_name text, company text, synced_at timestamptz DEFAULT now() ); - At least one HubSpot contact with lifecycle stage Lead.
Finished workflow
The Slack summary on the done port is optional; it's added in the last step.
Step-by-step
1. Create the workflow
On the Workflows page, click Create Workflow and name it HubSpot leads hourly sync.
2. Add the Scheduler
Add a Scheduler trigger:
- Type: Cron
- Cron:
0 * * * *(the top of every hour) - Timezone: yours
3. Search HubSpot for leads
Add a HubSpot node and name it Find leads:
- Credential: your HubSpot credential
- Resource: Contact
- Operation: Search
- filterGroups:
[{ "filters": [{ "propertyName": "lifecyclestage", "operator": "EQ", "value": "lead" }] }] - sorts:
[{ "propertyName": "createdate", "direction": "DESCENDING" }] - properties:
["email", "firstname", "lastname", "company"] - limit:
100
The matching contacts are in {{ $node["Find leads"].output.data.results }}. Each one has an id and a properties object.
4. Loop over the results
Add a Loop node after Find leads:
- Mode:
array - Field to loop on:
{{ $node["Find leads"].output.data.results }}
Because the value is a single expression, it resolves to the real list. Inside the loop body, the current contact is {{ $input.item }}.
5. Upsert each contact
Connect the Loop's loop output to a Database node named Upsert lead, and connect that node back into the Loop:
- Mode: Remote, with your Postgres connection details
- Read only: off
- Operation: Insert
- SQL:
INSERT INTO hubspot_leads (hubspot_id, email, first_name, last_name, company) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (hubspot_id) DO UPDATE SET email = EXCLUDED.email, first_name = EXCLUDED.first_name, last_name = EXCLUDED.last_name, company = EXCLUDED.company, synced_at = now() - Parameters:
{{ $input.item.id }}{{ $input.item.properties.email }}{{ $input.item.properties.firstname }}{{ $input.item.properties.lastname }}{{ $input.item.properties.company }}
ON CONFLICT makes the sync safe to repeat: a contact seen twice updates its row instead of adding a second one.
6. Save and activate
Save, then set the workflow to Active in the editor toolbar's version menu. The Scheduler fires at the next hour.
Try it
- Click Run in the editor to run it once now.
- Watch the nodes turn green. The Loop runs the Database node once per contact.
- Query the table:
SELECT * FROM hubspot_leads ORDER BY synced_at DESC; - Run it again. The row count stays the same and
synced_atmoves forward.
Variations
Weekdays only
Cron: 0 * * * 1-5 (every hour, Monday to Friday).
Post a summary
Connect the Loop's done output to a Slack node that posts "HubSpot sync finished" to your ops channel.
More than 100 leads
HubSpot returns the next page cursor in {{ $node["Find leads"].output.data.paging.next.after }}. Pass it as after in a second search, or loop with a While node until no cursor comes back.
Alert on failure
Set the Database node's error mode to retry in Node Settings, so a brief connection drop does not fail the run. See Retries & error handling.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Loop runs zero times | No contacts match the filter | Check that some contacts have lifecycle stage Lead |
[EXPR-ERROR: Node "Find leads" not found …] |
The HubSpot node has a different name | Rename the node to Find leads, or update the expressions |
| Database node refuses the insert | Read only is on | Turn Read only off |
| Columns are empty | The property is missing from properties |
Add it to the search's properties list |
Next
Found something out of date? This page lives in the Flero docs content set.