Two-CRM Contact Sync
Two Zaps keep the same contacts matching across Zoho CRM and a second system, Zapier Tables standing in for “CRM B”, whichever side someone edits. Copying the data was easy. Stopping each Zap’s write from looking like a fresh edit to the other one took three designs.
All 7 builds
- 3 loop-guard designs before one held
- 13 runs in two minutes from the loop version 2 caused
- 20 sec window that tells an echo from a real edit
The problem
Two systems, one customer, and an echo
Sales works in Zoho CRM; a support team works in another tool. The same customer lives in both, and when her phone number changes in one, the other copy goes stale.
Zapier has no two-way sync, so it takes two one-way Zaps. Built naively they loop: Zap 1’s write is a change in the table, so Zap 2 fires and writes back to Zoho, which fires Zap 1 again. Each pass is a billed task doing nothing.
The fix
Compare two timestamps, allow 20 seconds
continue if Last Synced At does not exist // never synced: first sync
or Last Edited is after Last Synced At + 20s // a person edited it
// otherwise the edit landed within 20s of our own write: it’s the echo, stopWhy a flag wasn’t enough
A “last synced by” flag never clears. After one sync, the next real edit is blocked too, and clearing the flag is itself a write that triggers the other Zap.
Why timestamps work
Time moves on by itself, so nothing needs resetting. Each Zap only reads its own trigger data and writes nothing to the record it’s checking.
| Version | How it decided “echo or edit?” | What happened |
|---|---|---|
| v1 · Flag | Skip if Last Synced By is the partner Zap | Stopped echoes, but also blocked the next real edit |
| v2 · Flag + reset | On an echo, reset the flag | The reset was a write: a real loop, 13 runs in about two minutes |
| v3 · Timestamp window | Pass if never synced, or edited 20 s+ after the stamp | Real edits sync both ways; echoes stop at the filter |
Under the hood
Two mirrored Zaps, matched on email
Contacts match on email, not name: two people can share a name, not an inbox.




Zoho Free can’t add fields, so the standard Description field was relabelled “Last Synced At” and holds the time as ISO text, which the filter can still compare as a date.
Live test
A real edit goes through, its echo stops
| Time (Sep 21) | Zap | Triggered by | Result |
|---|---|---|---|
| 4:38:57 pm | 1 · Zoho → Table | Phone number edited by hand in Zoho | Passed; row updated and stamped |
| 4:39:04 pm | 2 · Table → Zoho | Zap 1’s own write, 7 seconds earlier | Filtered; nothing written back |
| — | — | No further runs | Loop closed |


Edge case: a contact created first in the table ran the chain three times in about 30 seconds, then stopped on its own. Recorded as a known first-creation quirk instead of adding logic for a one-off that ends itself.
What broke
What broke, and how I found out
A loop guard that blocked real edits
Version 1 stopped echoes, but on the first live test a real phone change never reached the table. The record still said “written by the sync” from earlier.
A marker that never expires has to be cleared, and clearing is a write.

The fix that caused the infinite loop
Version 2 reset the flag on every echo. The reset was a write, so it fired the same Zap again, which read the fresh flag as a real edit and pushed it across. Thirteen runs in about two minutes, until I switched both Zaps off.
In a two-way sync, every write is a trigger.

Not shipping a known flaw
Going back to v1 and writing up its flaw was the safe option. I didn’t want to present a sync that silently drops real edits, so I built the timestamp design. When a later error pulled me back toward Paths, I kept the filter-only version: both cases lead to the same next step, so a branch would only add places to break.
The math step crashed on a blank
Subtracting Unix timestamps failed for never-synced contacts with “missing arguments”. Date steps tolerate blanks and the filter compares dates directly, so the math went away.
Design for the first run, when fields are still empty.

A timestamp stored as “09/21/26 03:44PM”
Writing Zapier’s raw current time into a text field stored a readable string the next comparison couldn’t parse. A format step to ISO 8601 before the write fixed it.
Eight hours in the future
The stamp showed Shanghai clock digits labelled as UTC. With both timezones set to UTC the format step only relabelled. Setting the source timezone to Asia/Shanghai forced a real conversion.
“UTC to UTC” is a label, not a conversion.


A live echo slipped through on a three-day-old date
The filter compared against Sep 18 because the step was bound to an old cached sample. Retesting the trigger first pulled fresh data.
Open the step’s own data in and out instead of trusting a summary.

Known limits
Trade-offs I chose
The 20-second window
A real edit within 20 s of a sync is treated as an echo. Rare for people, but not impossible.
Last write wins
If both sides change at nearly the same moment, the later sync overwrites the other.
Email is the match key
Changing an email on one side creates a second contact.
Not a sync product
Zapier itself recommends a dedicated tool for high-volume, real-time two-way sync. This build shows the logic.
TaskFlow and every company and person shown are fictional mock data. Built on Zoho CRM Free, Slack and a Zapier Professional trial; the Zaps are switched off now the trial has ended, and every screenshot comes from the real Zapier, Zoho and Slack accounts.
Hiring, or have a process to automate?
