APN Sync — Conflicts and Resolution
How conflicts are detected
FlowState checks, on every inbound APN event, whether HubSpot and AWS have each been modified since the last successful sync:
| HubSpot modified since last sync? | AWS modified since last sync? | Outcome |
|---|---|---|
| No | No | No-op. Nothing to do. |
| No | Yes | Apply AWS changes to HubSpot. No conflict. |
| Yes | No | HubSpot has newer data. Ignore the inbound event (next outbound cycle reconciles). |
| Yes | Yes | Both sides changed — a candidate conflict, confirmed per field (below). |
The comparison is per-deal. Different deals can be in different conflict states independently.
Both sides changing does not by itself mean a conflict
Each side may have changed a different field. The mapped user-editable field is the close date, so FlowState confirms a real conflict by comparing the close date on both sides against the value they last agreed on:
- Only HubSpot changed the close date (you edited it; AWS only changed, say, its review status on approval) → not a conflict. The edit is left intact and the next outbound cycle pushes it to APN. APN’s stale date never overwrites your edit.
- Only APN changed the close date → not a conflict. It is mirrored to HubSpot.
- Both changed the close date to different values → a real conflict (below).
So approving an opportunity in APN while a close-date edit is pending in HubSpot does not raise a false conflict.
Field ownership and conflict scope
Only conflict-detected fields (fields owned by “Both can update”) can enter a conflict state. These fields are:
LifeCycle.Stage(mapped to/fromdealstage)LifeCycle.TargetCloseDate(mapped to/fromclosedate)ExpectedCustomerSpend.Amount(mapped to/fromapn_mrr) — the partner drives APN with it, and AWS re-forecasts it during co-sell reviewCustomer.Account.Address.PostalCode(mapped to/fromapn_postal_code) — the partner’s own address correction and AWS’s can race the same wayLifeCycle.NextSteps(not mapped to a HubSpot property by default; can be added viaset_apn_field_mapping)
apn_mrr and apn_postal_code are confirmed the same way as closedate: compared per-field against the last-synced value, not just by timestamp.
AWS-owned fields (e.g., LifeCycle.ReviewStatus) are always applied from APN to HubSpot without conflict detection — they cannot conflict because HubSpot is not the source of truth for them.
HubSpot-owned fields (e.g., dealname, Customer.*, Project.*) are always taken from HubSpot — inbound APN events do not overwrite them.
Conflict states
Each deal has a conflict state:
| Value | Meaning |
|---|---|
none | No active conflict. Sync is running normally. |
hubspot_newer | Only HubSpot has been updated since the last sync. Resolved automatically on the next outbound sync. Not shown as a conflict in the UI. |
aws_newer | Only AWS has been updated. Applied automatically by the inbound handler. Not shown as a conflict in the UI. |
both_diverged | Both HubSpot and AWS have been updated on a conflict-detected field. Requires manual resolution. apn_sync_status is set to conflict on the HubSpot deal. |
Conflict notifications
When both_diverged is detected, FlowState:
- Sets
apn_sync_status = conflicton the HubSpot deal record. - Records the conflict, listing each conflicting field with the HubSpot value and the AWS value — read by the conflict-resolution page below, not mirrored to a HubSpot property.
- Sends a
apn.conflict_detectednotification to subscribed tenant members (email and Slack, if configured). - Pauses outbound sync for the affected deal until the conflict is resolved.
- Continues processing all other deals normally.
Resolving conflicts
Option 1 — UI conflict resolution page
Go to Deals → APN Sync → Conflicts in FlowState. The conflict page shows a three-pane diff for each conflicting field:
- Left pane: the value in HubSpot.
- Center pane: the value at the last successful sync (the common ancestor).
- Right pane: the value in AWS.
Choose Use HubSpot value, Use AWS value, or (for fields that support it) Set a custom value.
Click Resolve and resume sync to apply the resolution. FlowState writes the winning value to both sides and re-enqueues the deal for a reconciliation sync.
Option 2 — MCP tool
Use resolve_apn_conflict(hubspot_deal_id, resolution) where resolution is one of:
"prefer_hubspot"— all conflict-detected fields take the HubSpot value."prefer_aws"— all conflict-detected fields take the AWS value."merge"— specify per-field resolutions viamerge_fields: {"LifeCycle.Stage": "prefer_hubspot", "LifeCycle.TargetCloseDate": "prefer_aws"}.
Option 3 — Get conflict detail first
Use get_apn_conflict_detail(hubspot_deal_id) to retrieve the side-by-side diff programmatically before calling resolve_apn_conflict.
After resolution
- The conflict state is cleared:
conflict_state = none,apn_sync_status = pending. - FlowState enqueues a reconciliation sync for the deal (treated as a full sync).
- The reconciliation applies the resolved values to both HubSpot and APN.
- On success,
apn_sync_statusadvances tosubmittedorlive, depending on where the opportunity is in its lifecycle.
Sync resumes automatically — no further action is required after clicking Resolve.