Skip to main content

Documentation Index

Fetch the complete documentation index at: https://nango.dev/docs/llms.txt

Use this file to discover all available pages before exploring further.

Sync functions keep external API data fresh in your app. They run on a recurring cadence, write records to Nango’s records cache, and let your app consume changes reliably. Use sync functions when your app needs to replicate a dataset from an external system for sync, indexing, RAG, reporting, or change-detection workflows.

Create the function

Add a file under the integration’s syncs/ folder:
crm/syncs/contacts.ts
import { createSync } from 'nango';
import * as z from 'zod';

const Contact = z.object({
    id: z.string(),
    email: z.string().email().optional(),
    updatedAt: z.string()
});

const Metadata = z.object({
    accountRegion: z.string().optional()
});

const Checkpoint = z.object({
    lastUpdatedAt: z.string()
});

export default createSync({
    description: 'Syncs contacts',
    version: '1.0.0',
    frequency: 'every hour',
    autoStart: true,
    models: { Contact },
    metadata: Metadata,
    checkpoint: Checkpoint,
    exec: async (nango) => {
        const metadata = await nango.getMetadata();
        const checkpoint = await nango.getCheckpoint();
        const since = checkpoint?.lastUpdatedAt;

        for await (const page of nango.paginate<any>({
            endpoint: '/contacts',
            params: {
                ...(since ? { updated_after: since } : {}),
                ...(metadata?.accountRegion ? { region: metadata.accountRegion } : {})
            },
            paginate: {
                type: 'cursor',
                cursor_path_in_response: 'pagination.next_cursor',
                cursor_name_in_request: 'cursor',
                response_path: 'contacts',
                limit_name_in_request: 'limit',
                limit: 100
            }
        })) {
            const contacts = page.map((contact) => ({
                id: contact.id,
                email: contact.email,
                updatedAt: contact.updated_at
            }));

            await nango.batchSave(contacts, 'Contact');

            const lastContact = contacts[contacts.length - 1];
            if (lastContact) {
                await nango.saveCheckpoint({ lastUpdatedAt: lastContact.updatedAt });
            }
        }
    }
});
Import the function from index.ts:
index.ts
import './crm/syncs/contacts';
Sync implementation quality matters. Suboptimal syncs can consume far more API calls, compute, memory, and storage than needed; they can also become expensive, crash, behave unreliably, or hit provider rate limits. Before deploying a sync to production, review the Sync efficiency guide.
Before generating a sync, collect the integration ID, connection ID for testing, target records, provider pagination style, incremental filter, deletion strategy, required metadata, and expected sync frequency.Prefer incremental syncs when the provider supports them. Save records page by page, checkpoint after successful writes, and keep the record model limited to fields the app actually needs.

Pass parameters with metadata

Use connection metadata for any value the sync needs on scheduled runs:
  • Customer configuration, such as selected folders, projects, accounts, regions, or custom field mappings.
  • Provider context discovered after authorization.
  • Feature flags or filters that change what the sync fetches.
If the sync should not run before configuration exists, set autoStart: false, save the required metadata from your app, then start the schedule after configuration is complete. From your app, call the Node SDK nango.startSync() method:
await nango.startSync('crm', ['contacts'], '<CONNECTION-ID>');
Or call the Start sync API from any backend. Starting a sync schedule also triggers an immediate first run.

Test and deploy

Dry run the function against a real connection:
nango dryrun contacts '<CONNECTION-ID>' -e dev
If the sync reads connection metadata, pass test metadata to the dry run:
nango dryrun contacts '<CONNECTION-ID>' -e dev --metadata '{"accountRegion":"eu"}'
Or load metadata from a file:
nango dryrun contacts '<CONNECTION-ID>' -e dev --metadata @fixtures/metadata.json
Simulate resuming from a checkpoint:
nango dryrun contacts '<CONNECTION-ID>' -e dev --checkpoint '{"lastUpdatedAt":"2026-01-01T00:00:00.000Z"}'
Print local diagnostics for memory and CPU behavior:
nango dryrun contacts '<CONNECTION-ID>' -e dev --diagnostics
Deploy your functions:
nango deploy
To deploy only this sync function:
nango deploy --sync contacts dev

Start or trigger runs

If autoStart: true, Nango starts the schedule for new connections automatically. Otherwise, start it after the connection is ready and required metadata is saved:
await nango.startSync('crm', ['contacts'], '<CONNECTION-ID>');
nango.startSync() is a Node SDK method. Use the Start sync API if you are starting schedules from another backend language. Trigger a run immediately:
await nango.triggerSync('crm', ['contacts'], '<CONNECTION-ID>');
nango.triggerSync() is a Node SDK method. Use the Trigger sync API for one-off sync runs from another backend language.

Consume records

Configure your app’s Nango webhook URL in Environment Settings > Webhook URLs. After a run, Nango sends a sync webhook with the connection, integration, sync name, model, and change counts. Your app should then read records by cursor:
import { Nango } from '@nangohq/node';

const nango = new Nango({ secretKey: process.env.NANGO_SECRET_KEY! });

const result = await nango.listRecords({
    providerConfigKey: 'crm',
    connectionId: '<CONNECTION-ID>',
    model: 'Contact',
    cursor: '<last-processed-cursor>'
});
The consumer loop is: receive a sync webhook from Nango, look up the last cursor processed for that connection and model, fetch records with that cursor, process them, then store the cursor from the last record fetched.

Two-way syncs

You can implement two-way syncs by combining sync functions with action functions:
  • Use a sync function to replicate external data into your app and keep your local state fresh.
  • Use action functions for writes back to the external API, such as creating, updating, or deleting records.
  • After a write action succeeds, update your local state optimistically or trigger the relevant sync so the records cache catches up with the external source of truth.
This keeps the read path and write path explicit. The sync owns ongoing data freshness, checkpoints, record storage, and change delivery. Actions own user-initiated writes and provider-specific validation or side effects. For unified models, use the same schema for synced records and write action input where possible. This keeps your app from branching on each provider while still letting each provider implementation handle API-specific fields and edge cases.

Deep dives