Who this is for. Integration engineers building a sync that writes in both directions, such as with a CRM or an ERP. After this page you can write an ownership table for your integration and build jobs that cannot loop.
The two-way sync loop
A two-way sync without ownership rules loops. The site changes a contact's phone number and pushes it; the CRM saves it, which is a change, and its webhook sends the contact back; the site saves that, which is an update, and pushes again. With a second person editing on the other side, the two systems overwrite each other's edits in turn, and which value survives depends on timing.
Decide an owner per field, and record it
Decide, per field, which system is authoritative, and write it down where the next developer will look: in the job's Description and in the repository. An example for a CRM:
| Field | Owner | Site writes it | CRM writes it |
|---|---|---|---|
| Name, email, phone | Site (the member edits them) | Always | Never; ignored if sent |
| Marketing consent | Site (captured on the form) | Always | Never |
| Account owner, lifecycle stage | CRM | Never | Always |
| CRM id | CRM | Once, into externalId, when the contact is created | Never changes it |
Enforce it in the jobs
- Each direction sends only the fields its side owns. The push never includes the account owner; the pull never includes the phone number.
- A value arriving for a field the sender does not own is dropped, not merged.
- Compare before writing. A pull that finds the value already equal writes nothing, so an echo of your own change dies at the first hop.
Comparing before writing is also how a job avoids starting itself: a condition that compares the value before and after the change, as in Guarding against recursion.
Know which of your writes start jobs
Whether the site's half of the loop exists depends on how the write is made:
- A pull running as an asynchronous job writes from a background worker, and a record written there fires no entity events. It cannot start the site's push jobs, which breaks the loop on the site's side by itself.
- A pull that arrives as a synchronous web event, because the other system needs the reply, runs inside the web request, and its writes fire entity events like an editor's. It needs the comparison above, or the recursion guard.
- A write through the site's REST API, made by a job with an HTTP Request, is a web request too, so it fires entity events even when the job that sent it is asynchronous. The worked example relies on a condition for exactly that reason.
Record actions cannot write orders, invoices, quotes or form submissions, so a sync of those with a CRM or an ERP always goes through the REST API (The pattern catalogue), and always fires entity events on the site.
When the two systems disagree
Ownership also settles the argument after an incident. When the CRM and the site disagree, the owner's value is right, and the fix is to re-sync that field from its owner, not to decide by hand record by record.