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.

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)

Showcase builds

19 complete workflows from my own projects, each with its n8n workflow JSON to import. Showcase entries link the file at the end of the article.

See the showcase builds

Keep reading

191 entries grouped by topic, from first workflow to queue mode. Free, no signup.

Browse the encyclopedia

Need it built?

I design, build and run n8n systems for clients. Every engagement starts with a $1,500 diagnostic audit, credited toward the build.

Book a 20-minute call