Package Library
Installable package Verified

List Sync

Sync records from your CRM to your Dialer.

@quickflo/list-syncby @quickflov2.0.2
See how List Sync works

Package overview

Overview

Synchronises records between your CRM and your outbound dialer. Real-time edits show up in the dialer within seconds. Bulk imports are buffered to a queue and drained at the dialer's rate limit, so they don't slow real-time traffic.

The integration handles the things that usually break this kind of pipeline: duplicate records when the source retries, queue backlogs during outages, dialer rate limits during large imports, and worker restarts mid-processing.


Currently supported pairs

CRM Dialer Setup guide
Salesforce Five9 https://docs.quickflo.app/solutions/list-sync/salesforce/

The rest of this README documents the Salesforce → Five9 integration. If you need a different pair, contact your QuickFlo account team.


What it does

  • Real-time edits in Salesforce land in the matching Five9 list within seconds.
  • Bulk imports (Data Loader, mass edits) flow through a separate queue at Five9's bulk rate limit. They don't slow real-time traffic.
  • Duplicates from CRM-side retries are recognised and skipped.
  • Records persist in their queue before the webhook returns 200. Worker restarts and network blips don't lose them.
  • Pause is non-destructive. The CRM keeps sending while paused; records queue; everything drains on unpause.
  • Dry-run mode runs the whole flow except the dialer call. Useful for validating field mappings or replaying a record.
  • Failed records carry a typed reason code. Re-queue them in bulk after fixing the root cause.
  • Pre-built dashboards for queue health, throughput, and error breakdown ship with the package.

Quick start

Setting up Salesforce → Five9 takes about 30 minutes if you have your Five9 admin credentials and field mappings ready.

  1. Create a Five9 connection in QuickFlo named five9. The credentials must be from a Five9 user with:
    • The Admin role, with User can use administrator services enabled
    • Contact upload permission
    • List upload permission
  2. Import this list-sync package into QuickFlo. Workflows, data stores, dashboards, and the webhook trigger all come in together.
  3. Install the QuickFlo Apex package in Salesforce. Setup at https://docs.quickflo.app/solutions/list-sync/salesforce/.
  4. Seed your settings and field mappings. Default templates are included; see the Settings and Field mappings sections below.
  5. Copy the webhook URL from the trigger's detail page. Configure your Salesforce Apex package to use it.

Test with dryRun: true first: edit a Salesforce record and watch for an entry in list_sync_logs. When that works, flip dryRun: false, edit another record, and verify it appears in the Five9 list.

For bulk: run a small Data Loader test (10 to 50 records) and watch list_sync_bulk_chunks for incoming chunks.


Day-to-day use

Three operator controls, all from the QuickFlo UI.

Pause

Set paused: true in settings to stop all processing. The webhook keeps accepting records; they just queue up until you unpause.

For finer control: pausedTrickle: true stops only real-time, pausedBulk: true stops only bulk. Useful for Five9 maintenance or a credential rotation that only affects one path.

Dry-run

Set dryRun: true to run everything end-to-end except the actual Five9 call. Useful for testing field mappings, replaying a problematic record, or validating routing before going live.

Retry failed records

The salesforce-five9-list-sync-retry-failed workflow re-queues every record currently in the failed state back to pending, up to a limit. Run it from the QuickFlo UI after you've fixed the root cause (rotated credentials, corrected a mapping, etc.) and the dispatcher will re-process them on its next cycle.

Scoped re-queue by errorKind (e.g. only FIVE9_INVALID_ACCOUNT) or by newerThan a timestamp is a planned enhancement — the current workflow re-queues all failed records.


Settings

The list_sync_settings row with key "five9" controls operational behavior. The package ships with sensible defaults; the tables below describe what each knob does.

Master controls

Field Default Purpose
paused false Stop all processing
pausedTrickle false Stop only real-time processing
pausedBulk false Stop only bulk processing
dryRun false Run everything except the actual Five9 call
idempotencyTtlSeconds 604800 (7 days) How long to remember a record's event ID for dedup

Real-time path

Field Default Purpose
maxAttempts 3 Retries per record before marking it failed
dispatcherBatchLimit 25 Records processed per dispatcher cycle
retryBaseBackoffSeconds 30 Wait between retries
trickle.debounceMs 3000 Collapses the near-simultaneous Salesforce INSERT+UPDATE burst into one Five9 call. Events for the same record land on one queue row (newest wins); the row isn't claimable until this much quiet has passed. Adds up to debounceMs of latency to every real-time sync — set it above your observed INSERT→UPDATE gap but low enough to still feel real-time.

Bulk path

Field Default Purpose
bulk.maxAttempts 3 Retries per chunk before marking it failed
bulk.dispatcherBatchLimit 10 Chunks processed per dispatcher cycle
bulk.retryBaseBackoffSeconds 60 Wait between retries

Coalescing window (bulk.coalesce)

For Bulk delivery, many small chunks that arrive within a time window are coalesced into the fewest possible 50k-record CSV uploads by the flush-window workflow. These knobs tune that behavior.

Field Default Purpose
maxWaitSeconds 30 How long a window stays open collecting chunks before it flushes. Higher = better coalescing (fewer Five9 calls), more delay before upload.
maxChunksPerFlush 500 Chunks claimed per flush. Bounds memory per flush execution (peak ≈ maxChunksPerFlush × avg records/chunk × fan-out ratio). A backlog beyond this drains via re-kick plus the recovery sweep.
maxUploadRecords 50000 Five9's hard per-call cap. The merged, post-fan-out payload is blind-sliced into groups no larger than this. Don't raise above 50000.
uploadConcurrency 4 How many slices upload concurrently through the shared rate limiter, to keep the UPLOADING token bucket saturated. Currently hardcoded to 4 inside flush-window; this setting is forward-looking until numeric-template coercion is confirmed.

Five9 API pacing

The modes object controls how aggressively the integration calls Five9.

Method Default Notes
UPLOAD_SINGLE (real-time) 0 (no pacing) Inline retries handle transient throttles. Set to ~500 ms only if you see sustained throttling (Five9 allows ~7200 single-record calls/hour).
UPLOADING (bulk) 3000 ms (1 call / 3 sec = 20 / min) Burst-paced for low latency. Five9 allows 20 uploads/min in a burst but only ~400/hour sustained, so a bulk that keeps uploading for more than ~20 minutes will hit the hourly cap at this setting. Bump to 9000 ms (1 call / 9 sec ≈ 400/hour) for long-running bulks that need to pace safely and indefinitely. Each upload carries up to 50k records, so the limit you hit is calls-per-hour, not record count.

For the full math on rate limits and throughput, see Solution Limits.

Five9 sync modes

These control the Five9 list-update semantics applied to every upload (Five9's CRM add/update and list add/delete modes). The defaults match the common "keep the Five9 list in step with the CRM" case; change them only if you understand the corresponding Five9 behavior.

Field Default Purpose
crmAddMode ADD_NEW How records new to the CRM are added to Five9
crmUpdateMode UPDATE_ALL How existing records are updated in Five9
listAddMode ADD_ALL How records are added to the target Five9 list
listDeleteMode DELETE_ALL How deletes propagate to the Five9 list

Field mappings

The list_sync_field_mappings table holds one row per Salesforce object you sync. Add one for Lead, Contact, and any custom objects you want to push into Five9. A starter set is in data-stores/list_sync_field_mappings.json.

A Lead, organized by campaign, routes to a different Five9 list per campaign:

{
  "key": "Lead",
  "value": {
    "fieldMappings": {
      "fields": {
        "FirstName": "first_name",
        "LastName": "last_name",
        "Phone": "number1",
        "Email": "email",
      },
      "primaryKeys": ["number1"],
      "callNowField": "",
      "callTimeField": "",
    },
    "listNameByCampaign": {
      "Spring Outreach 2026": "spring_outbound_leads",
      "Fall Re-engagement": "fall_reengagement_leads",
    },
    "listName": "unsorted_leads", // fallback for any campaign not listed above
  },
}

A Contact, not campaign-organized, just goes to one list:

{
  "key": "Contact",
  "value": {
    "fieldMappings": {
      "fields": {
        "FirstName": "first_name",
        "LastName": "last_name",
        "Phone": "number1",
      },
      "primaryKeys": ["number1"],
    },
    "listName": "contacts_master",
  },
}
  • fields maps Salesforce field names to Five9 column names. All Five9-relevant fields go here.
  • primaryKeys identifies records in Five9 (used by the crmAddMode / crmUpdateMode matching described under Five9 sync modes).

Dialing records immediately (call now / call time)

Five9 reserves two list columns, call_now and call_time, for immediate-dial scheduling. They aren't regular fields, so Five9 rejects an upload that contains either column unless it's properly enabled. The mapping controls this per object:

  • callNowMode — Five9 enum, default "NONE". Set to NEW_CRM_ONLY, NEW_LIST_ONLY, or ANY to dial matching records as they're added. While it's "NONE", the call_now column is dropped from the upload automatically, so a leftover mapping to it never breaks the sync.
  • callNowField — the column that flags call-now (map a Salesforce field to call_now to provide the per-record value). Sent only when callNowMode is not "NONE".
  • callTimeField — the column carrying a scheduled call time (map a Salesforce field to call_time). When this is empty, the call_time column is dropped from the upload for the same reason.

So you enable the feature by setting callNowMode (and/or callTimeField); leave them at their defaults and the reserved columns simply never reach Five9. This applies on both paths — bulk drops the columns from the CSV, real-time strips them from the record.

Call-now ON — dial new records as they're added:

{
  "key": "Lead",
  "value": {
    "fieldMappings": {
      "fields": {
        "FirstName": "first_name",
        "LastName": "last_name",
        "Phone": "number1",
        "DialImmediately": "call_now", // Salesforce field -> the reserved call_now column
      },
      "primaryKeys": ["number1"],
      "callNowField": "call_now", // designate which column is the call-now flag
      "callTimeField": "",
      "callNowMode": "ANY", // ON. NONE = off; NEW_CRM_ONLY / NEW_LIST_ONLY also valid
    },
    "listName": "hot_leads",
  },
}

Call-now OFF — set callNowMode back to "NONE" (the default). The call_now column is then dropped from every upload automatically, even if a call_now field mapping is left in place — so flipping the switch is the only change needed. Same for call-time: a non-empty callTimeField turns it on, empty turns it off.

Which Five9 list a record lands in

The list name is resolved in priority order, so each object type just populates the layer that fits it. All of this is configured per object row in list_sync_field_mappings (not in list_sync_settings):

  1. _list_name_override on the record itself — a Five9 column. Set it by mapping a Salesforce field to _list_name_override in that object's fieldMappings.fields, or by writing it in the record-builder extension point. Highest priority; lets one record target a specific list. Real-time only (bulk groups records into one CSV per list).
  2. listNameByCampaign[campaign] — the listNameByCampaign object on the object's row in list_sync_field_mappings (a sibling of fieldMappings). Routes by Salesforce campaign membership; populate this for campaign-organized objects like Lead.
  3. listName — the listName string on the object's row in list_sync_field_mappings (also a sibling of fieldMappings). The object-type default, used when there's no campaign match (or no campaign map at all). This is the simple "one list per object" case, like Contact.
  4. Campaign name — the last-resort fallback if none of the above resolve. Automatic; nothing to configure.

So every layer except the per-record override is just a field on that object's row in list_sync_field_mappings — the same rows the Field mappings examples above show. The same chain runs on both the real-time and bulk paths (bulk skips step 1), so a record routes the same way no matter which path delivers it.


Customizing records (and splitting one record into many)

Most syncs map one Salesforce record to one Five9 record. When you need more control, the package ships a single extension point: the salesforce-five9-list-sync-record-builder workflow. Edit it once and it applies to both the real-time and bulk paths.

It receives an array of { mapped, raw } pairs and returns the array of Five9 records to send:

  • mapped is the record after field mapping (Five9 column names). Your starting point.
  • raw is the original Salesforce record, including fields that were not mapped. Use it to reach values like extra phone columns.

Return one record per input for a 1:1 reshape, or return more to fan out. Every record you return from one source record is sent to that source record's list.

Example: split a lead's phone fields into separate Five9 records

A Salesforce lead often carries several phone numbers (Phone, MobilePhone, and so on). To dial each as its own Five9 record, open the record-builder workflow and use the phone-split example already included there:

const PHONES = [
  'Phone',
  'MobilePhone',
  'OtherPhone',
  'HomePhone',
  'AssistantPhone',
];
return {
  records: initial.records.flatMap((r) =>
    PHONES.map((field) => r.raw[field])
      .filter(Boolean)
      .map((number1) => ({ ...r.mapped, number1 })),
  ),
};

Good to know:

  • The default is a 1:1 passthrough, so leaving the record-builder untouched preserves expected behavior.
  • All records produced from one lead go to the same Five9 list. Routing split records to different lists is not supported.
  • A split lead is deleted the same way it was added: a delete fans out across the same records, so nothing is orphaned.
  • Dry-run still covers the whole flow, including the fan-out, without calling Five9.
  • On the bulk path, the CSV columns come from your field mapping, so introducing a brand-new Five9 column through the record-builder takes effect on the real-time path; on the bulk path, keep to the mapped columns.

Monitoring

The package includes pre-built dashboards covering queue health, throughput, error breakdown, and recent activity. You can see what's pending, what's failed, and why, without writing queries.


Troubleshooting

Records aren't reaching Five9 at all. Check that paused and the path-specific pause flag are both false. Verify the Five9 connection is healthy. Check the webhook trigger's recent invocations to confirm Salesforce is reaching it.

Records failing with FIVE9_INVALID_ACCOUNT or FIVE9_AUTH_FAILED. Five9 credentials need rotation or the connection needs updating. Once fixed, run retry-failed to re-queue the failed records.

Records failing with FIVE9_INVALID_ARGUMENT or FIVE9_NOT_FOUND. Field mapping or list name issue. Check list_sync_field_mappings for the object type. Verify the Five9 list exists with the expected name and that the mapped columns exist on that list.

Records failing with FIVE9_RATE_LIMITED during large bulks. Your bulk volume is hitting Five9's hourly cap. Three options: bump modes.UPLOADING.rateLimitIntervalMs from 3000 to 9000 for smoother pacing, ask Five9 support to raise the rate limit on your tenant, or spread the bulk across a longer window. The retry-failed workflow can clean up the failed chunks once you've adjusted.

Bulk chunks stuck in in_flight status. A worker crashed during processing. Run the recovery workflow to reset them to pending. The dispatcher will pick them up on its next cycle.


How it works (for the curious)

You don't need to know any of this to use the integration. It's here for engineers who want to understand the design.

There are two paths into the integration:

Real-time path. Someone edits one record in Salesforce. Salesforce posts that one record to QuickFlo. QuickFlo writes it to a durable queue, returns 200 to Salesforce, and a background worker drains the queue by calling Five9's per-record API.

Bulk path. Salesforce sends a chunk of records (typically 200 from a Data Loader operation). QuickFlo writes the chunk to a separate durable queue, returns 200, and opens (or joins) a short coalescing window for that destination. When the window flushes, a background worker claims every buffered chunk for that window at once, merges them, slices the result into Five9's 50k-record CSV limit, and uploads the slices concurrently through a shared rate limiter. Coalescing many small deliveries into the fewest possible uploads is what keeps a steady stream of Data Loader chunks from exhausting Five9's hourly upload cap.

The two paths use independent Five9 API quotas, so a large bulk import never slows down real-time edits.

Both paths use the same primitives for reliability:

  • Idempotency: each record carries a unique event ID; if Salesforce retries, the integration recognizes the duplicate and skips it.
  • Atomic claim: workers grab records from the queue atomically, so two workers can run in parallel without claiming the same record.
  • Durable persistence: records are in the queue before the webhook returns 200, so a worker crash mid-processing leaves them recoverable.
  • Error classification: Five9 errors are mapped to a known taxonomy (rate limited, auth failed, invalid input, etc.) so the right thing happens automatically (retry transient errors, halt immediately on permanent ones).

For the full architecture writeup including state diagrams and durability guarantees, see Architecture deep-dive.


What's in this package

Workflows

Workflow What it does
salesforce-five9-list-sync-webhook Receives all traffic from Salesforce
salesforce-five9-list-sync-dispatcher Processes the real-time queue
salesforce-five9-process-record Handles each individual real-time record
salesforce-five9-list-sync-bulk-ingress Accepts bulk chunks from Salesforce; opens a coalescing window and schedules its flush
salesforce-five9-list-sync-flush-window Drains one coalescing window: claims its buffered chunks, fans out, blind-slices to Five9's 50k cap, uploads slices concurrently through the shared rate limiter, finalizes each source chunk. Replaces the old bulk-dispatcher + process-chunk pair (1 chunk = 1 upload)
salesforce-five9-list-sync-record-builder Extension point: builds and optionally fans out the Five9 records per source record (both paths)
salesforce-five9-list-sync-recovery One-button heal-everything-stuck: re-queues crashed trickle + bulk work and re-dispatches orphaned windows
salesforce-five9-list-sync-retry-failed Re-queues records that failed terminally

Setup utilities

Run manually, not part of the sync path:

Workflow What it does
list-sync-ingest-contact-fields Pulls your Five9 contact-field schema and caches the valid column names into list_sync_field_mappings (key Five9). Run once during setup so you know which Five9 columns to map to.
list-sync-get-list-contacts Runs Five9's "List Details" report for one or more named lists and returns their current contents as rows. Use it to verify or reconcile what's actually in a Five9 list.

Data stores

Table Contents
list_sync_settings Your operational config
list_sync_field_mappings Salesforce-to-Five9 field mappings
list_sync_queue Records in the real-time queue
list_sync_bulk_chunks Chunks in the bulk queue
list_sync_bulk_windows Coalescing window markers (one open row per active window)
list_sync_idempotency Dedup keys (TTL'd)
list_sync_rate_limits Rate-limit pacing state
list_sync_logs Audit trail of every sync attempt

Dashboards

  • Queue health (pending, in-flight, done, failed counts)
  • Throughput (records synced per hour, by path)
  • Error breakdown (failures grouped by reason)
  • Recent activity (latest sync results)

Webhook trigger

One webhook URL. The Salesforce-side package posts every payload to it.


Reference

Salesforce → Five9

Other CRM/dialer pairs

Not yet supported. Contact your QuickFlo account team to scope a new pair.

What’s inside

One install, the complete system.

Workflows

10 included

  • Salesforce Five9 List Sync Bulk Ingress
  • Salesforce Five9 List Sync Recovery
  • Salesforce Five9 List Sync Dispatcher
  • Salesforce Five9 List Sync Retry Failed
  • Salesforce Five9 List Sync Webhook
  • Salesforce Five9 Process Record
  • Salesforce Five9 List Sync Flush Window
  • Salesforce Five9 List Sync Record Builder
  • List Sync Get List Contacts
  • List Sync Ingest Contact Fields

Triggers

2 included

  • Salesforce Five9 List Sync
  • List Sync Ingest Contact Fields Schedule

Dashboards

2 included

  • List Sync Queue
  • List Sync Dashboard

Data stores

6 included

  • List Sync Settings
  • List Sync Field Mappings
  • List Sync Logs
  • List Sync Queue
  • List Sync Bulk Chunks
  • List Sync Bulk Windows

Configure during install

Set up what it needs. QuickFlo handles the wiring.

You’ll connect or provide these requirements during guided setup, before anything changes.

Connections

1 connection to map during installation

  • Five9

Extension points

Designed for your team to make their own.

This package includes clearly defined areas your team can customize without forking or rebuilding the rest of the solution.

Record Builder

Choose during setup

Build the Five9 records to send for each source record. Return one record per input to reshape 1:1, or many to fan out (for example, split a lead's multiple phone fields into separate Five9 records). Applies to both add and delete; every returned record is sent to the source record's resolved list.