Skip to Content

Scheduled exports

A scheduled export reads a set of HubSpot records on a timer and writes them to a CSV file, one file per day. It is built from three pieces you already know plus two step actions:

scheduled source (cron) -> query (HubSpot, one day of records) -> export_file (CSV)
  • a scheduled source emits a tick on its cron schedule;
  • a query step reads every record that matches its filters and falls in one calendar day;
  • an export_file step writes those records as a CSV file named after that day.

The file is kept in storage plugsync manages, in a folder dedicated to the connector, for 30 days (see How long the files are kept). You download it from the connector’s Exports section in the dashboard, or with the API (see Downloading the files). Delivery to a server of your own (SFTP or similar) and formats other than CSV are not available.

Full config, sources included: _examples/scheduled-export-to-file.json, reproduced here:

{ "version": "3.0", "entities": [ { "name": "contact", "schemas": { "hubspot": { "object_type": "contacts", "field_mappings": [] }, "export": { "field_mappings": [ {"source": "$._item.id", "target": "hubspot_id"}, {"source": "$._item.properties.email", "target": "email"}, {"source": "$._item.properties.firstname", "target": "first_name"}, {"source": "$._item.properties.lastname", "target": "last_name"} ] } } } ], "flows": [ { "name": "daily_new_customers_export", "source": "nightly", "event_type": "tick", "steps": [ {"id": "customers", "action": "query", "target": "hubspot", "entity": "contact", "filters": [{"property": "lifecyclestage", "operator": "EQ", "value": "customer"}], "properties": ["email", "firstname", "lastname"], "window": {"property": "hs_lifecyclestage_customer_date", "timezone": "Europe/Rome", "offset_days": -1}}, {"id": "write_csv", "action": "export_file", "rows": {"each": "$customers.results", "entity": "contact", "schema": "export"}, "key": "daily/new_customers_{$customers.window.date}.csv", "delimiter": ";"} ] } ], "sources": [ { "name": "nightly", "type": "scheduled", "config": {"inbound": {"schedule": "0 2 * * *"}} } ], "settings": {} }

Every night at 02:00 UTC the nightly source emits a tick. customers reads the contacts that became customers the day before, in Rome time; write_csv writes them to daily/new_customers_<YYYY-MM-DD>.csv, one row per contact, columns hubspot_id;email;first_name;last_name.

The schedule

Set config.inbound.schedule on the scheduled source. The cron expression is read in UTC. Without a schedule the source emits a tick every 30 seconds, and every tick runs the query again and rewrites the file: for an export, always set one.

The tick’s payload carries one value, $.payload.scheduled_for: the time its cron schedule named, as an ISO 8601 UTC timestamp. It is the only input the export takes from the tick, and it decides the day the query reads (see The day window).

query

Reads every HubSpot record of an entity that matches the filters, page after page until the last one.

FieldRequiredDescription
targetyesMust be hubspot.
entityyesEntity whose hubspot schema gives the object type (object_type).
filtersnoConditions, all of which must hold (AND). Each is {"property", "operator", "value"}, see below. Omitted, every record of the object type matches.
propertiesnoHubSpot property names to read on each record. Omitted, HubSpot’s default set for the object type.
windownoRestricts the query to one calendar day, see The day window.
max_recordsnoThe largest result the step accepts, from 1 to 10000 (default 10000).

Filter operators, by the value they take:

Operatorsvalue
EQ, NEQ, LT, LTE, GT, GTE, CONTAINS_TOKEN, NOT_CONTAINS_TOKENOne value, or a $-ref to one. A boolean is sent as true/false, the spelling HubSpot matches.
IN, NOT_INA non-empty list, or a $-ref to one.
HAS_PROPERTY, NOT_HAS_PROPERTYNone: omit value.

A step accepts at most 6 filters, and a window counts as 2: that is the most HubSpot accepts in one search. A filter whose $-ref resolves to nothing fails the step instead of being dropped, since dropping it would widen the query.

The step output, readable by later steps as $<step-id>.*:

KeyContent
resultsThe list of records, each as HubSpot returns it: {"id": "...", "properties": {...}}.
totalHow many records matched.
window{"date": "YYYY-MM-DD", "start": "...", "end": "..."} when the step has a window (start and end as ISO 8601 timestamps with the timezone offset), otherwise null.

Limit: HubSpot’s search stops at 10000 results, and its match count stops there too. A query that matches more than max_records records fails, without writing anything, rather than exporting a truncated list; so does a query that matches 10000 records or more whatever max_records says, since HubSpot then cannot tell how many there are. The largest day a query can export is therefore 9999 records. Narrow the filters or the window.

Consistency: the query reads the result page by page. A record created, changed or deleted in HubSpot while the pages are being read can be missed or read twice, so the list is exact only for records that stay still during the scan. Reading a day that has already ended (the default offset_days: -1) keeps this to records edited during those few seconds.

The day window

"window": {"property": "hs_lifecyclestage_customer_date", "timezone": "Europe/Rome", "offset_days": -1}
KeyRequiredDefaultDescription
propertyyesA date-time property of the object.
timezonenoUTCIANA timezone name (Europe/Rome, America/New_York, …) the day is counted in.
offset_daysno-1Which day, counted from the day of the tick’s scheduled time: -1 is the day before, 0 the same day, down to -366.

The query then keeps only the records whose property falls from midnight to midnight of that day, in that timezone, and exposes the day as $<step-id>.window.date. On the day a daylight-saving change happens the window is 23 or 25 hours long, as the calendar in that timezone has it.

The tick’s scheduled time is the time its cron schedule named, not the moment it was processed: a tick that runs late, or is replayed days later, still reads the day of its own schedule. A source without a schedule fires on every check, and its time is the moment it fired.

With offset_days: 0 the file covers a day that has not ended yet, so it misses whatever happens after the tick. Keep the default -1 for a complete daily file.

Date-only properties: HubSpot stores a date without a time as midnight UTC. In a timezone behind UTC (America/New_York, …) that midnight falls on the evening before, so a record dated the 22nd lands in the window of the 21st. For a date-only property, use "timezone": "UTC".

export_file

Writes a list of records to a CSV file, replacing the file if it already exists.

FieldRequiredDescription
rowsyesThe records to write: an each node, or a $-ref to a list of objects. A ref to a step’s output ($customers.results) must name a step that comes earlier in the flow, or the draft is not saved.
keyyesRelative path of the file inside the connector’s folder, for example daily/contacts_{$customers.window.date}.csv.
columnswith a $-ref rowsColumn order, each a field name or {"field": "...", "header": "..."}.
formatnocsv, the only format.
delimiternoOne character between values (default ,): ;, a tab, … A quote or a line break is not accepted.
headernoWrite the header row (default true).
escape_formulasnoWrite a value that starts with =, +, -, @, a tab or a carriage return with a leading ', so a spreadsheet shows it as text (default false). See Opening the file in a spreadsheet.

Rows from an each node

"rows": {"each": "$customers.results", "entity": "contact", "schema": "export"}

Each record of the list goes through entities.<entity>.schemas.<schema>.field_mappings, the same way an each node works in a post body: inside the mappings $._item is the current record, so $._item.properties.email reads its email. fields (optional) keeps only the listed mapping targets.

Without columns, the columns are the mapping targets, in the order the mappings declare them, restricted to fields when set. A mapping that builds its target at runtime (target_template) cannot name a column in advance: with such a schema, declare columns.

Rows from a $-ref

"rows": "$customers.results", "columns": [{"field": "id", "header": "HubSpot ID"}, {"field": "properties.email", "header": "Email"}]

columns is required. A dotted field reads a nested value (properties.email on a HubSpot record).

Cells

  • A missing value is an empty cell; a key the record carries beyond the columns is left out.
  • true and false are written as such; a nested object or list is written as JSON.
  • The file is UTF-8 without a byte-order mark, with CRLF line endings; a value containing the delimiter, a quote or a line break is quoted.

Opening the file in a spreadsheet

Excel, LibreOffice and Google Sheets run a cell that starts with =, +, - or @ as a formula. A value typed into HubSpot by anyone, such as a name or a company field filled in from a web form, can therefore run as a formula on the computer of whoever opens the file. When the file is meant to be opened in a spreadsheet, set "escape_formulas": true: such values are written with a leading ', which the spreadsheet shows as text. Negative numbers get the ' too ('-5), so leave the option off when a program reads the file and expects the values as they are in HubSpot. The header row is not changed.

The file name

Each {$...} placeholder in key is replaced with the value it resolves to, which must be a plain name: letters, digits, ., _ and -. $customers.window.date (2026-09-22) is one. A value that would add a folder or is empty fails the step. key cannot start or end with /, contain .., or reference $.credential.

The step output:

KeyContent
pathThe file’s path inside the connector’s folder, as resolved from key.
rowsHow many data rows were written (the header not included).
size_bytesThe file’s size.

Re-runs and empty days

  • Same tick, same file. The day comes from the tick’s scheduled time, which travels with the tick: a retry after a failure, a tick processed late or a tick replayed on a later day reads the same day, and export_file writes the same path. The file is replaced, never duplicated.
  • Same day, same file. Two ticks that fall on the same day (a schedule that fires more than once a day) also write the same path. The last one wins. To keep one file per run, put something that differs per run in key, or use a schedule that fires once a day.
  • No records, still a file. When the query finds nothing, export_file still writes the file, with the header row only (an empty file with "header": false). An empty day is then distinguishable from a day the export did not run.

How long the files are kept

  • 30 days from the last write. A file is deleted automatically 30 days after it was last written; the deletion itself can land up to a day later. A file rewritten by a later run (same path) starts its 30 days again. The period is the same for every plan.
  • Deleting the connector deletes its files at once, with the connector.
  • Closing the organization deletes the files of all its connectors at once.

A deleted file cannot be recovered: download the files you need to keep within 30 days, for example with a script right after the nightly run.

Downloading the files

In the dashboard, open the connector and choose Exports under Runtime: the list shows each file with its path, last change and size, newest first, and Download saves it.

To fetch the files from a script, for example right after the nightly run, use an organization API key (or a session token of a signed-in user). Any member of the organization can list and download; a connector that belongs to another organization answers 404, exactly like an id that does not exist.

List the connector’s files:

curl https://app.plugsync.com/api/connectors/{connector_id}/exports \ -H "Authorization: Bearer <organization API key>"
{ "files": [ {"path": "daily/new_customers_2026-09-22.csv", "size_bytes": 48213, "last_modified": "2026-09-23T02:00:07Z"}, {"path": "daily/new_customers_2026-09-21.csv", "size_bytes": 47790, "last_modified": "2026-09-22T02:00:06Z"} ], "truncated": false, "listing_incomplete": false }

path is the file’s path inside the connector’s folder, the value the export_file step resolved from key. The list returns up to 1000 files, newest first, picked over the whole folder. truncated: true means the folder holds more files than the ones returned; the others are older and each can still be downloaded by its path. A folder is scanned up to 100000 files: past that, listing_incomplete: true means the scan did not reach every file, so the list is the newest of the part that was read and newer files may be missing from it. The dashboard shows the same notes. Downloading by path works for every file whatever the list shows, since the path follows from the step’s key.

Get a download link for one file, passing its path (URL-encoded):

curl -G https://app.plugsync.com/api/connectors/{connector_id}/exports/download \ --data-urlencode "path=daily/new_customers_2026-09-22.csv" \ -H "Authorization: Bearer <organization API key>"
{"url": "https://...", "expires_in_seconds": 300}

The url downloads the file with a plain GET and no Authorization header, and stops working after expires_in_seconds (five minutes): ask for a new link each time rather than storing one.

StatusMeaning
404 Connector not foundThe connector does not exist or belongs to another organization.
404 export_not_foundNo file at that path in the connector’s folder.
422 invalid_export_pathpath is empty, absolute, contains .., empty segments or a backslash.
503 exports_unavailableFile downloads are not available right now; retry later.

Errors

What happensOutcome
HubSpot rate limitThe tick is retried later, once the portal’s quota allows it. The retry reads the query again from its first page.
HubSpot or the file storage temporarily unavailable (5xx, throttling, timeouts)The tick is retried.
The HubSpot connection is no longer authorized (401, 403)The connector stops until it is reconnected.
HubSpot rejects the search (400: an unknown property, a value of the wrong type)The tick fails without retry.
More records than max_records (or 10000 or more), a $-ref that resolves to nothing (rows included: a wrong step name or path never passes as an empty day), a file name value that is not a plain name, a record that is not an objectThe tick fails without retry.
File storage not available for the connector, or the write refusedThe tick fails without retry.

A failed tick writes nothing. The next scheduled tick runs normally, for its own day.

Trying it without writing

A draft dry-run of the flow runs both steps against stubs: the query returns no records and the file is not written, so the trace shows export_file with rows: 0 and the path it would write.

Last updated on