Bidirectional sync and conflict resolution
A bidirectional connector moves data both ways: your source system writes into HubSpot, and changes made by your team inside HubSpot flow back out. When the same field on the same record is touched from both sides close together in time, something has to decide who wins. This page explains how Plugsync resolves that, exactly, and where the honest limits are.
Avoid the conflict first: field ownership
The cheapest way to resolve a conflict is to make it structurally impossible. If a field is only ever supposed to be edited on one side (say, internal_notes is CRM-only, vat_number is ERP-only), restrict the inbound write to the fields your source actually owns using the entity’s fields allowlist. A field that’s never written by the inbound path can never be overwritten by it, regardless of which conflict strategy is active downstream.
The other structural mitigation is sending deltas, not snapshots. If your source system posts “what changed” instead of “the full current state” on every event, there’s nothing to fight over for the fields it didn’t touch. Snapshot-style payloads (common with older ERPs that re-send the whole record on a timer) are the case where the strategies below matter most, and where their limits (see below) are most visible.
Conflict resolution is the safety net for the fields that are genuinely editable from both sides. Use it for those, not as a substitute for field ownership.
The three strategies
Conflict resolution is per-property, not per-record: on the same sync event, one field can be kept from HubSpot while another is written from the source, depending on what each field’s own history says.
source_wins (default)
The inbound write always applies. This is the pre-existing behavior and the default when no conflicts block is configured: zero extra API calls, zero behavior change for connectors that don’t need this.
target_wins
Stateless rule: a field whose most recent HubSpot value was written by someone or something other than Plugsync stays as-is in HubSpot, no matter when either side touched it. Once the field belongs to a CRM edit, the inbound write for that field is dropped on every subsequent sync, until HubSpot’s own record shows Plugsync as the last writer again (for example, after a sync that wasn’t in conflict).
most_recent
Per-field timestamp comparison: the HubSpot value wins only if it is strictly newer than the source’s edit time for that event. Otherwise the inbound value is written. The source edit time is resolved through a fallback chain:
conflicts.source_modified_path, if configured (a JSON path into your event payload, e.g. an ERP’s own “last modified” field);- the event’s
occurred_at(from the Event API timestamp or the HubSpot journal), if the path above isn’t configured or doesn’t resolve; - the time Plugsync ingested the event, as a last resort.
In both target_wins and most_recent, every field identified as contested is recorded as a resolved conflict, whichever side won.
Configuring it
Conflict resolution is a top-level block in the connector config, alongside entities and flows:
{
"version": "3.1",
"conflicts": {
"strategy": "most_recent",
"source_modified_path": "$.payload.updated_at"
}
}
strategy:source_wins(default) |target_wins|most_recent.source_modified_path: optional. Recommended whenever your source system has its own audit timestamp (anupdated_at, alast_modified, a change-log entry). Without it,most_recentfalls back to when the event arrived, which is a weaker proxy for when the field actually changed at the source, especially for ERPs that emit periodic snapshots rather than change events.
The block goes through the normal draft, review, and publish flow, same as any other config change.
Seeing what got resolved
Every field the detection identifies as contested is persisted as a resolved conflict, visible in the connector’s Conflicts tab: which field, the value on each side, which strategy resolved it, and which value was kept. Nothing is silently dropped without a record.
The honest limits
- A small race window remains. Detection reads the field’s HubSpot history right before writing it, but there’s no conditional write on HubSpot’s CRM API, so the read and the write aren’t atomic. The gap is milliseconds, not the ~10 seconds of queue latency it replaces, but it isn’t zero.
- Event time is a proxy, not the real edit time, for snapshot-style sources. If your ERP re-sends the full record on a schedule rather than emitting a change the moment a field is edited, the event’s timestamp only tells you when the snapshot was taken, not when that specific field last changed. Declaring
source_modified_pathnarrows this gap when the source can tell you; without it,most_recentis comparing against ingest time, which favors HubSpot more often than it should. - Detection costs one extra read. With
target_winsormost_recentactive, each inbound update makes one additional HubSpot API call to fetch field history before writing (batched in groups of 50 records on the bulk path).source_winsmakes none. Factor this into your rate-limit budget if you’re running near HubSpot’s ceiling.
What’s next
- Read Getting started if you haven’t connected a bidirectional connector yet.
- Read the Agent guide for how to ask the AI agent to configure
conflictsandfieldsfor you. - Visit Troubleshooting if a sync isn’t behaving the way you expect.