> ## 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 function guide

> Build sync functions that keep external API data fresh and consumable by your app.

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:

```typescript crm/syncs/contacts.ts theme={null}
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`:

```typescript index.ts theme={null}
import './crm/syncs/contacts';
```

<Warning>
  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](/guides/functions/syncs/sync-efficiency).
</Warning>

<Accordion title="For agents">
  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.
</Accordion>

## Pass parameters with metadata

Use [connection metadata](/guides/functions/storage#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](/reference/backend/backend-sdk/node#start-schedule-for-syncs):

```ts theme={null}
await nango.startSync('crm', ['contacts'], '<CONNECTION-ID>');
```

Or call the [Start sync API](/reference/backend/http-api/sync/start) from any backend. Starting a sync schedule also triggers an immediate first run.

## Test and deploy

Dry run the function against a real connection:

```bash theme={null}
nango dryrun contacts '<CONNECTION-ID>' -e dev
```

If the sync reads connection metadata, pass test metadata to the dry run:

```bash theme={null}
nango dryrun contacts '<CONNECTION-ID>' -e dev --metadata '{"accountRegion":"eu"}'
```

Or load metadata from a file:

```bash theme={null}
nango dryrun contacts '<CONNECTION-ID>' -e dev --metadata @fixtures/metadata.json
```

Simulate resuming from a checkpoint:

```bash theme={null}
nango dryrun contacts '<CONNECTION-ID>' -e dev --checkpoint '{"lastUpdatedAt":"2026-01-01T00:00:00.000Z"}'
```

Print local diagnostics for memory and CPU behavior:

```bash theme={null}
nango dryrun contacts '<CONNECTION-ID>' -e dev --diagnostics
```

Deploy your functions:

```bash theme={null}
nango deploy
```

To deploy only this sync function:

```bash theme={null}
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:

```typescript theme={null}
await nango.startSync('crm', ['contacts'], '<CONNECTION-ID>');
```

`nango.startSync()` is a [Node SDK method](/reference/backend/backend-sdk/node#start-schedule-for-syncs). Use the [Start sync API](/reference/backend/http-api/sync/start) if you are starting schedules from another backend language.

Trigger a run immediately:

```typescript theme={null}
await nango.triggerSync('crm', ['contacts'], '<CONNECTION-ID>');
```

`nango.triggerSync()` is a [Node SDK method](/reference/backend/backend-sdk/node#trigger-syncs). Use the [Trigger sync API](/reference/backend/http-api/sync/trigger) 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:

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    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>'
    });
    ```
  </Tab>

  <Tab title="cURL">
    ```bash theme={null}
    curl -G https://api.nango.dev/records \
      --header 'Authorization: Bearer <ENV-SECRET-KEY>' \
      --header 'Provider-Config-Key: crm' \
      --header 'Connection-Id: <CONNECTION-ID>' \
      --data-urlencode 'model=Contact' \
      --data-urlencode 'cursor=<last-processed-cursor>'
    ```
  </Tab>
</Tabs>

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](/guides/functions/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

* [Sync efficiency](/guides/functions/syncs/sync-efficiency)
* [Checkpoints](/guides/functions/syncs/checkpoints)
* [Records cache](/guides/functions/syncs/records-cache)
* [Deletion detection](/guides/functions/syncs/deletion-detection)
* [Real-time syncs](/guides/functions/syncs/realtime-syncs)
* [Sync partitioning](/guides/functions/syncs/sync-partitioning)
