Tips > Building Workflows

Build a Two-Way Sync Pattern Between Systems

Two-way sync keeps two systems in parity: changes in System A propagate to System B, and changes in System B propagate to System A.

TipIntermediate3 min read

Two-way sync keeps two systems in parity: changes in System A propagate to System B, and changes in System B propagate to System A. The key challenge is avoiding infinite loops (A changes B, which triggers a change back to A). The solution: track a sync timestamp, compare records by unique key, and only update records that have actually changed since the last sync.

Real-world example: Sync a Notion database (project tracker) with an Airtable base (client-facing dashboard). When a project status changes in either system, the other system updates.

// Inputs: notionRecords (from Notion API) and
//         airtableRecords (from Airtable API)
const notionItems = $('Notion - Get Projects').all();
const airtableItems = $('Airtable - Get Projects').all();
const staticData = $getWorkflowStaticData("global");
const lastSync = staticData.lastSyncTimestamp || 0;

const toUpdateInAirtable = [];
const toUpdateInNotion = [];
const toCreateInAirtable = [];
const toCreateInNotion = [];

// Build lookup maps by shared unique key (project_id)
const notionMap = new Map();
notionItems.forEach(item => {
  notionMap.set(item.json.project_id, item.json);
});

const airtableMap = new Map();
airtableItems.forEach(item => {
  airtableMap.set(item.json.fields.project_id, item.json);
});

// Compare records
for (const [projectId, notionRecord] of notionMap) {
  const airtableRecord = airtableMap.get(projectId);

  if (!airtableRecord) {
    // Exists in Notion but not Airtable -- create in Airtable
    toCreateInAirtable.push({ json: notionRecord });
    continue;
  }

  const notionUpdated = new Date(notionRecord.last_edited).getTime();
  const airtableUpdated = new Date(
    airtableRecord.fields.last_modified
  ).getTime();

  if (notionUpdated > airtableUpdated && notionUpdated > lastSync) {
    // Notion is newer -- update Airtable
    toUpdateInAirtable.push({
      json: {
        airtable_record_id: airtableRecord.id,
        ...notionRecord
      }
    });
  } else if (airtableUpdated > notionUpdated && airtableUpdated > lastSync) {
    // Airtable is newer -- update Notion
    toUpdateInNotion.push({
      json: {
        notion_page_id: notionRecord.notion_id,
        ...airtableRecord.fields
      }
    });
  }
}

// Check for records in Airtable but not in Notion
for (const [projectId, airtableRecord] of airtableMap) {
  if (!notionMap.has(projectId)) {
    toCreateInNotion.push({ json: airtableRecord.fields });
  }
}

// Update sync timestamp
staticData.lastSyncTimestamp = Date.now();

// Output to four branches
return [toUpdateInAirtable, toUpdateInNotion,
        toCreateInAirtable, toCreateInNotion];
```text
```text title="Workflow Structure"
Schedule Trigger (every 5 min)
  |
  +-> Notion - Get All Projects
  +-> Airtable - Get All Projects
  |
  Merge (wait for both)
  |
  Code Node (sync logic above) -> 4 outputs:
    Output 1 -> Airtable - Update Records
    Output 2 -> Notion - Update Pages
    Output 3 -> Airtable - Create Records
    Output 4 -> Notion - Create Pages
```text
> **Warning: Conflict Resolution**
>
> When the same record is modified in both systems between sync intervals, you have a conflict. The pattern above uses "last write wins" based on timestamp. For more sophisticated conflict resolution, add a review queue: flag conflicting records and send them to a human via Slack or email for manual resolution.

This pattern generalizes to any pair of systems that expose list and update operations.

**Related:** [Use Path Parameters in Webhook URLs for Dynamic Routing](../webhook-mastery/01-use-path-parameters-in-webhook-urls-for-dynamic-routing.md) | [Use Edit Fields in "Map Each" Mode for Simple Renames](../data-transformation/01-use-edit-fields-in-map-each-mode-for-simple-renames.md)

Want this running in your stack?

I build production n8n and Cloudflare automation for teams — the same engineering behind HarperFlow. Fixed-price, escrow-protected, US-based.