nango-function-builder

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Nango Function Builder

Nango Function 构建工具

Build deployable Nango functions (actions, syncs, on-event hooks) with repeatable patterns and validation steps.
使用可重复的模式和验证步骤构建可部署的Nango functions(actions、syncs、事件钩子)。

When to use

使用场景

  • User wants to build or modify a Nango function
  • User wants to build an action in Nango
  • User wants to build a sync in Nango
  • User wants to build an on-event hook (validate-connection, post-connection-creation, pre-connection-deletion)
  • 用户希望构建或修改Nango function
  • 用户希望在Nango中构建Action
  • 用户希望在Nango中构建Sync
  • 用户希望构建事件钩子(validate-connection、post-connection-creation、pre-connection-deletion)

Useful Nango docs (quick links)

实用Nango文档(快速链接)

Workflow (recommended)

推荐工作流

  1. Verify this is a Zero YAML TypeScript project (no
    nango.yaml
    ) and you are in the Nango root (
    .nango/
    exists).
  2. Compile as needed with
    nango compile
    (one-off).
  3. Create/update the function file under
    {integrationId}/actions/
    ,
    {integrationId}/syncs/
    , or
    {integrationId}/on-events/
    .
  4. Register the file in
    index.ts
    (side-effect import).
  5. Validate with
    nango dryrun ... --validate
    .
  6. Record mocks with
    nango dryrun ... --save
    and generate tests with
    nango generate:tests
    .
  7. Run
    npm test
    .
  8. Deploy with
    nango deploy dev
    .
  1. 验证当前是Zero YAML TypeScript项目(无
    nango.yaml
    ),且处于Nango根目录(存在
    .nango/
    )。
  2. 按需使用
    nango compile
    编译(一次性操作)。
  3. {integrationId}/actions/
    {integrationId}/syncs/
    {integrationId}/on-events/
    下创建/更新函数文件。
  4. index.ts
    中注册文件(副作用导入)。
  5. 使用
    nango dryrun ... --validate
    验证。
  6. 使用
    nango dryrun ... --save
    记录模拟数据,并使用
    nango generate:tests
    生成测试用例。
  7. 运行
    npm test
  8. 使用
    nango deploy dev
    部署。

Preconditions (Do Before Writing Code)

前置条件(编写代码前完成)

Confirm TypeScript Project (No nango.yaml)

确认TypeScript项目(无nango.yaml)

This skill only supports TypeScript projects using createAction()/createSync()/createOnEvent().
bash
ls nango.yaml 2>/dev/null && echo "YAML PROJECT DETECTED" || echo "OK - No nango.yaml"
If you see YAML PROJECT DETECTED:
  • Stop immediately.
  • Tell the user to upgrade to the TypeScript format first.
  • Do not attempt to mix YAML and TypeScript.
本技能仅支持使用createAction()/createSync()/createOnEvent()的TypeScript项目。
bash
ls nango.yaml 2>/dev/null && echo "YAML PROJECT DETECTED" || echo "OK - No nango.yaml"
如果显示YAML PROJECT DETECTED:
  • 立即停止操作。
  • 告知用户先升级到TypeScript格式。
  • 不要尝试混合使用YAML和TypeScript。

Verify Nango Project Root

验证Nango项目根目录

Do not create files until you confirm the Nango root:
bash
ls -la .nango/ 2>/dev/null && pwd && echo "IN NANGO PROJECT ROOT" || echo "NOT in Nango root"
If you see NOT in Nango root:
  • cd into the directory that contains .nango/
  • Re-run the check
  • Do not use absolute paths as a workaround
All file paths must be relative to the Nango root. Creating files with extra prefixes while already in the Nango root will create nested directories that break the build.
在确认Nango根目录前不要创建文件:
bash
ls -la .nango/ 2>/dev/null && pwd && echo "IN NANGO PROJECT ROOT" || echo "NOT in Nango root"
如果显示NOT in Nango root:
  • 切换到包含
    .nango/
    的目录
  • 重新运行检查
  • 不要使用绝对路径作为替代方案
所有文件路径必须相对于Nango根目录。如果已在根目录下仍使用额外前缀创建文件,会生成嵌套目录导致构建失败。

Project Structure and Naming

项目结构与命名规范

./
|-- .nango/
|-- index.ts
|-- hubspot/
|   |-- actions/
|   |   `-- create-contact.ts
|   |-- on-events/
|   |   `-- validate-connection.ts
|   `-- syncs/
|       `-- fetch-contacts.ts
`-- slack/
    `-- actions/
        `-- post-message.ts
  • Provider directories: lowercase (hubspot, slack)
  • Action files: kebab-case (create-contact.ts)
  • Event handler files: kebab-case in
    on-events/
    (validate-connection.ts)
  • Sync files: kebab-case (many teams use a
    fetch-
    prefix, but it's optional)
  • One function per file (action, sync, or on-event)
  • All actions, syncs, and on-event hooks must be imported in index.ts
./
|-- .nango/
|-- index.ts
|-- hubspot/
|   |-- actions/
|   |   `-- create-contact.ts
|   |-- on-events/
|   |   `-- validate-connection.ts
|   `-- syncs/
|       `-- fetch-contacts.ts
`-- slack/
    `-- actions/
        `-- post-message.ts
  • 提供商目录:小写(hubspot、slack)
  • Action文件:短横线命名(create-contact.ts)
  • 事件处理器文件:在
    on-events/
    目录下使用短横线命名(validate-connection.ts)
  • Sync文件:短横线命名(很多团队使用
    fetch-
    前缀,但非强制)
  • 每个文件对应一个函数(action、sync或事件钩子)
  • 所有actions、syncs和事件钩子必须在index.ts中导入

Register scripts in
index.ts
(required)

index.ts
中注册脚本(必填)

Use side-effect imports only (no default/named imports). Include the
.js
extension.
typescript
// index.ts
import './github/actions/get-top-contributor.js';
import './github/syncs/fetch-issues.js';
import './github/on-events/validate-connection.js';
Symptom of incorrect registration: the file compiles but you see
No entry points found in index.ts...
or the function never appears.
仅使用副作用导入(无默认/命名导入)。需包含
.js
扩展名。
typescript
// index.ts
import './github/actions/get-top-contributor.js';
import './github/syncs/fetch-issues.js';
import './github/on-events/validate-connection.js';
注册错误的症状:文件编译成功,但出现
No entry points found in index.ts...
提示,或函数从未显示。

Decide: Action vs Sync vs OnEvent

选择:Action vs Sync vs OnEvent

Action:
  • One-time request, user-triggered
  • CRUD operations and small lookups
  • Thin API wrapper
Sync:
  • Continuous data sync on a schedule
  • Fetches all records or incremental changes
  • Uses batchSave/batchDelete
OnEvent:
  • Runs on connection lifecycle events (e.g., validate credentials)
  • Good for verification and setup/cleanup hooks
If unclear, ask the user which behavior they want (one-time vs scheduled vs lifecycle hook).
Action:
  • 一次性请求,由用户触发
  • CRUD操作和小型查询
  • 轻量API封装
Sync:
  • 按计划持续同步数据
  • 获取所有记录或增量变更
  • 使用batchSave/batchDelete
OnEvent:
  • 在连接生命周期事件触发时运行(如验证凭证)
  • 适用于验证和设置/清理钩子
如果不确定,询问用户需要哪种行为(一次性、定时或生命周期钩子)。

Required Inputs (Ask User if Missing)

必填输入(缺失时询问用户)

Always:
  • Integration ID (provider name)
  • Connection ID (for dryrun)
  • Function name (kebab-case)
  • API reference URL or sample response
Action-specific:
  • Use case summary
  • Input parameters
  • Output fields
  • Metadata JSON if required
  • Test input JSON for dryrun/mocks
Sync-specific:
  • Model name (singular, PascalCase)
  • Sync type (full or incremental)
  • Frequency (every hour, every 5 minutes, etc.)
  • Metadata JSON if required (team_id, workspace_id)
OnEvent-specific:
  • Event type (validate-connection, post-connection-creation, pre-connection-deletion)
  • Expected behavior (what to validate/change)
If any of these are missing, ask the user for them before writing code. Use their values in dryrun commands and tests.
通用必填项:
  • 集成ID(提供商名称)
  • 连接ID(用于dryrun)
  • 函数名称(短横线命名)
  • API参考URL或示例响应
Action特定项:
  • 用例摘要
  • 输入参数
  • 输出字段
  • 所需的元数据JSON
  • 用于dryrun/模拟的测试输入JSON
Sync特定项:
  • 模型名称(单数,大驼峰命名)
  • 同步类型(全量或增量)
  • 频率(每小时、每5分钟等)
  • 所需的元数据JSON(team_id、workspace_id)
OnEvent特定项:
  • 事件类型(validate-connection、post-connection-creation、pre-connection-deletion)
  • 预期行为(要验证/修改的内容)
如果以上任何项缺失,在编写代码前询问用户。将其值用于dryrun命令和测试。

Prompt Templates (Use When Details Are Missing)

提示模板(信息缺失时使用)

Action prompt:
Please provide:
Integration ID (required):
Connection ID (required):
Use Case Summary:
Action Inputs:
Action Outputs:
Metadata JSON (if required):
Action Name (kebab-case):
API Reference URL:
Test Input JSON:
Sync prompt:
Please provide:
Integration ID (required):
Connection ID (required):
Model Name (singular, PascalCase):
Endpoint Path (for Nango endpoint):
Frequency (every hour, every 5 minutes, etc.):
Sync Type (full or incremental):
Metadata JSON (if required):
API Reference URL:
Action提示:
请提供以下信息:
集成ID(必填):
连接ID(必填):
用例摘要:
Action输入:
Action输出:
元数据JSON(如需):
Action名称(短横线命名):
API参考URL:
测试输入JSON:
Sync提示:
请提供以下信息:
集成ID(必填):
连接ID(必填):
模型名称(单数,大驼峰命名):
端点路径(用于Nango端点):
频率(每小时、每5分钟等):
同步类型(全量或增量):
元数据JSON(如需):
API参考URL:

Non-Negotiable Rules (Shared)

不可协商的规则(通用)

Platform constraints (docs-backed)

平台约束(基于文档)

  • Zero YAML TypeScript projects do not use
    nango.yaml
    . Define functions with
    createAction()
    ,
    createSync()
    , or
    createOnEvent()
    .
  • Register every action/sync/on-event in
    index.ts
    via side-effect import (
    import './<path>.js'
    ) or it will not load.
  • You cannot install/import arbitrary third-party packages in Functions. Relative imports inside the Nango project are supported. Pre-included dependencies include
    zod
    ,
    crypto
    /
    node:crypto
    , and
    url
    /
    node:url
    .
  • Sync records must include a stable string
    id
    .
  • Action outputs cannot exceed 2MB.
  • deleteRecordsFromPreviousExecutions()
    is for full refresh syncs only. Call it only after you successfully fetched + saved the full dataset; do not swallow errors and still call it.
  • HTTP request retries default to
    0
    . Set
    retries
    intentionally (and be careful retrying non-idempotent writes).
  • Zero YAML TypeScript项目不使用
    nango.yaml
    。使用
    createAction()
    createSync()
    createOnEvent()
    定义函数。
  • 每个action/sync/事件钩子必须通过副作用导入(
    import './<path>.js'
    )在index.ts中注册,否则无法加载。
  • 不能在Functions中安装/导入任意第三方包。支持Nango项目内的相对导入。预包含的依赖有
    zod
    crypto
    /
    node:crypto
    url
    /
    node:url
  • Sync记录必须包含稳定的字符串
    id
  • Action输出不能超过2MB。
  • deleteRecordsFromPreviousExecutions()
    仅适用于全量刷新同步。仅在成功获取并保存完整数据集后调用;不要吞掉错误后仍调用该方法。
  • HTTP请求重试默认值为
    0
    。需有意设置
    retries
    (注意重试非幂等写入操作)。

Conventions (recommended)

约定(推荐)

  • Prefer explicit parameter names (
    user_id
    ,
    channel_id
    ,
    team_id
    ).
  • Add
    .describe()
    examples for IDs, timestamps, enums, and URLs.
  • Avoid
    any
    ; use inline types when mapping responses.
  • Prefer static Nango endpoint paths (avoid
    :id
    /
    {id}
    in the exposed endpoint); pass IDs in input/params.
  • Add an API doc link comment above each provider API call.
  • Standardize list actions on
    cursor
    /
    next_cursor
    .
  • For optional outputs, return
    null
    only when the output schema models
    null
    .
  • Use
    nango.zodValidateInput()
    when you need custom input validation/logging; otherwise rely on schemas +
    nango dryrun --validate
    .
Symptom of missing index.ts import: file compiles without errors but does not appear in the build output.
  • 优先使用明确的参数名称(
    user_id
    channel_id
    team_id
    )。
  • 为ID、时间戳、枚举和URL添加
    .describe()
    示例。
  • 避免使用
    any
    类型;映射响应时使用内联类型。
  • 优先使用静态Nango端点路径(避免在暴露的端点中使用
    :id
    /
    {id}
    );在输入/参数中传递ID。
  • 在每个提供商API调用上方添加API文档链接注释。
  • 列表类Action统一使用
    cursor
    /
    next_cursor
  • 对于可选输出,仅当输出模式定义了
    null
    时才返回
    null
  • 需要自定义输入验证/日志时使用
    nango.zodValidateInput()
    ;否则依赖模式验证+
    nango dryrun --validate
index.ts导入缺失的症状:文件编译无错误,但未出现在构建输出中。

Parameter Naming Rules

参数命名规则

  • IDs: suffix with _id (user_id, channel_id)
  • Names: suffix with _name (channel_name)
  • Emails: suffix with _email (user_email)
  • URLs: suffix with _url (callback_url)
  • Timestamps: use *_at or *_time (created_at, scheduled_time)
Mapping example (API expects a different parameter name):
typescript
const InputSchema = z.object({
    user_id: z.string()
});

const config: ProxyConfiguration = {
    endpoint: 'users.info',
    params: {
        user: input.user_id
    },
    retries: 3
};
  • ID:后缀加_id(user_id、channel_id)
  • 名称:后缀加_name(channel_name)
  • 邮箱:后缀加_email(user_email)
  • URL:后缀加_url(callback_url)
  • 时间戳:使用_at或_time(created_at、scheduled_time)
映射示例(API期望不同的参数名称):
typescript
const InputSchema = z.object({
    user_id: z.string()
});

const config: ProxyConfiguration = {
    endpoint: 'users.info',
    params: {
        user: input.user_id
    },
    retries: 3
};

Action Template (createAction)

Action模板(createAction)

Notes:
  • input
    is required even for "no input" actions. Use
    z.object({})
    .
  • Do not import
    ActionError
    as a value from
    nango
    (it is a type-only export in recent versions). Throw
    new nango.ActionError(payload)
    using the
    nango
    exec parameter.
  • ProxyConfiguration
    typing is optional. Only import it if you explicitly annotate a variable.
typescript
import { z } from 'zod';
import { createAction } from 'nango';

const InputSchema = z.object({
    user_id: z.string().describe('User ID. Example: "123"')
    // For no-input actions use: z.object({})
});

const OutputSchema = z.object({
    id: z.string(),
    name: z.union([z.string(), z.null()])
});

const action = createAction({
    description: 'Brief single sentence',
    version: '1.0.0',

    endpoint: {
        method: 'GET',
        path: '/user',
        group: 'Users'
    },

    input: InputSchema,
    output: OutputSchema,
    scopes: ['required.scope'],

    exec: async (nango, input): Promise<z.infer<typeof OutputSchema>> => {
        const response = await nango.get({
            // https://api-docs-url
            endpoint: '/api/v1/users',
            params: {
                userId: input.user_id
            },
            retries: 3 // safe for idempotent GETs; be careful retrying non-idempotent writes
        });

        if (!response.data) {
            throw new nango.ActionError({
                type: 'not_found',
                message: 'User not found',
                user_id: input.user_id
            });
        }

        return {
            id: response.data.id,
            name: response.data.name ?? null
        };
    }
});

export type NangoActionLocal = Parameters<(typeof action)['exec']>[0];
export default action;
注意:
  • 即使是“无输入”的Action也需要
    input
    。使用
    z.object({})
  • 不要从
    nango
    中导入
    ActionError
    作为值(在最新版本中它是仅类型导出)。使用
    nango
    执行参数抛出
    new nango.ActionError(payload)
  • ProxyConfiguration
    类型是可选的。仅在显式注解变量时导入。
typescript
import { z } from 'zod';
import { createAction } from 'nango';

const InputSchema = z.object({
    user_id: z.string().describe('用户ID。示例:"123"')
    // 无输入Action使用:z.object({})
});

const OutputSchema = z.object({
    id: z.string(),
    name: z.union([z.string(), z.null()])
});

const action = createAction({
    description: '简短单句描述',
    version: '1.0.0',

    endpoint: {
        method: 'GET',
        path: '/user',
        group: 'Users'
    },

    input: InputSchema,
    output: OutputSchema,
    scopes: ['required.scope'],

    exec: async (nango, input): Promise<z.infer<typeof OutputSchema>> => {
        const response = await nango.get({
            // https://api-docs-url
            endpoint: '/api/v1/users',
            params: {
                userId: input.user_id
            },
            retries: 3 // 幂等GET请求可设置为3;非幂等POST请求需谨慎重试
        });

        if (!response.data) {
            throw new nango.ActionError({
                type: 'not_found',
                message: '用户未找到',
                user_id: input.user_id
            });
        }

        return {
            id: response.data.id,
            name: response.data.name ?? null
        };
    }
});

export type NangoActionLocal = Parameters<(typeof action)['exec']>[0];
export default action;

Action Metadata (When Required)

Action元数据(按需使用)

Use metadata when the action depends on connection-specific values.
typescript
const MetadataSchema = z.object({
    team_id: z.string()
});

const action = createAction({
    metadata: MetadataSchema,

    exec: async (nango, input) => {
        const metadata = await nango.getMetadata<{ team_id?: string }>();
        const teamId = metadata?.team_id;

        if (!teamId) {
            throw new nango.ActionError({
                type: 'invalid_metadata',
                message: 'team_id is required in metadata.'
            });
        }
    }
});
当Action依赖连接特定值时使用元数据。
typescript
const MetadataSchema = z.object({
    team_id: z.string()
});

const action = createAction({
    metadata: MetadataSchema,

    exec: async (nango, input) => {
        const metadata = await nango.getMetadata<{ team_id?: string }>();
        const teamId = metadata?.team_id;

        if (!teamId) {
            throw new nango.ActionError({
                type: 'invalid_metadata',
                message: '元数据中需要team_id。'
            });
        }
    }
});

Action CRUD Patterns

Action CRUD模式

OperationMethodConfig Pattern
Createnango.post(config)data: { properties: {...} }
Readnango.get(config)endpoint:
resource/${id}
, params: {...}
Updatenango.patch(config)endpoint:
resource/${id}
, data: {...}
Deletenango.delete(config)endpoint:
resource/${id}
Listnango.get(config)params: {...} with pagination
Note: These endpoint examples are for ProxyConfiguration (provider API). The createAction endpoint path must stay static.
Recommended in most configs:
  • API doc link comment above endpoint
  • retries: set intentionally (often
    3
    for idempotent GET/LIST; avoid retries for non-idempotent POST unless the API supports idempotency)
Optional input fields pattern:
typescript
data: {
    required_field: input.required_field,
    ...(input.optional_field && { optional_field: input.optional_field })
}
操作方法配置模式
创建nango.post(config)data: { properties: {...} }
读取nango.get(config)endpoint:
resource/${id}
, params: {...}
更新nango.patch(config)endpoint:
resource/${id}
, data: {...}
删除nango.delete(config)endpoint:
resource/${id}
列表nango.get(config)params: {...} 包含分页参数
注意:这些端点示例适用于ProxyConfiguration(提供商API)。createAction的端点路径必须保持静态。
大多数配置中推荐:
  • 端点上方添加API文档链接注释
  • retries:有意设置(幂等GET/LIST通常设为3;非幂等POST除非API支持幂等性否则避免重试)
可选输入字段模式:
typescript
data: {
    required_field: input.required_field,
    ...(input.optional_field && { optional_field: input.optional_field })
}

Action Error Handling (ActionError)

Action错误处理(ActionError)

Use ActionError for expected failures (not found, validation, rate limit). Use standard Error for unexpected failures.
typescript
if (response.status === 429) {
    throw new nango.ActionError({
        type: 'rate_limited',
        message: 'API rate limit exceeded',
        retry_after: response.headers['retry-after']
    });
}
Do not return null-filled objects to indicate "not found". Use ActionError instead.
ActionError response format:
json
{
  "error_type": "action_script_failure",
  "payload": {
    "type": "not_found",
    "message": "User not found",
    "user_id": "123"
  }
}
对预期失败(未找到、验证错误、速率限制)使用ActionError。对意外失败使用标准Error。
typescript
if (response.status === 429) {
    throw new nango.ActionError({
        type: 'rate_limited',
        message: 'API速率限制已超出',
        retry_after: response.headers['retry-after']
    });
}
不要返回填充null的对象来表示“未找到”。应使用ActionError代替。
ActionError响应格式:
json
{
  "error_type": "action_script_failure",
  "payload": {
    "type": "not_found",
    "message": "用户未找到",
    "user_id": "123"
  }
}

Action Pagination Standard (List Actions)

Action分页标准(列表类Action)

All list actions must use cursor/next_cursor regardless of provider naming.
Schema pattern:
typescript
const ListInput = z.object({
    cursor: z.string().optional().describe('Pagination cursor from previous response. Omit for first page.')
});

const ListOutput = z.object({
    items: z.array(ItemSchema),
    next_cursor: z.union([z.string(), z.null()])
});
Provider mapping:
ProviderNative InputNative OutputMap To
Slackcursorresponse_metadata.next_cursorcursor -> next_cursor
Notionstart_cursornext_cursorcursor -> next_cursor
HubSpotafterpaging.next.aftercursor -> next_cursor
GitHubpageLink headercursor -> next_cursor
GooglepageTokennextPageTokencursor -> next_cursor
Example:
typescript
exec: async (nango, input): Promise<z.infer<typeof ListOutput>> => {
    const config: ProxyConfiguration = {
        endpoint: 'api/items',
        params: {
            ...(input.cursor && { cursor: input.cursor })
        },
        retries: 3
    };

    const response = await nango.get(config);

    return {
        items: response.data.items.map((item: { id: string; name: string }) => ({
            id: item.id,
            name: item.name
        })),
        next_cursor: response.data.next_cursor || null
    };
}
所有列表类Action必须使用cursor/next_cursor,无论提供商的命名方式如何。
模式示例:
typescript
const ListInput = z.object({
    cursor: z.string().optional().describe('上一次响应中的分页游标。首次请求可省略。')
});

const ListOutput = z.object({
    items: z.array(ItemSchema),
    next_cursor: z.union([z.string(), z.null()])
});
提供商映射:
提供商原生输入原生输出映射为
Slackcursorresponse_metadata.next_cursorcursor -> next_cursor
Notionstart_cursornext_cursorcursor -> next_cursor
HubSpotafterpaging.next.aftercursor -> next_cursor
GitHubpageLink headercursor -> next_cursor
GooglepageTokennextPageTokencursor -> next_cursor
示例:
typescript
exec: async (nango, input): Promise<z.infer<typeof ListOutput>> => {
    const config: ProxyConfiguration = {
        endpoint: 'api/items',
        params: {
            ...(input.cursor && { cursor: input.cursor })
        },
        retries: 3
    };

    const response = await nango.get(config);

    return {
        items: response.data.items.map((item: { id: string; name: string }) => ({
            id: item.id,
            name: item.name
        })),
        next_cursor: response.data.next_cursor || null
    };
}

OnEvent Template (createOnEvent)

OnEvent模板(createOnEvent)

Use on-event functions for connection lifecycle hooks:
  • validate-connection
    : verify credentials/scopes on connection creation
  • post-connection-creation
    : run setup after a connection is created
  • pre-connection-deletion
    : cleanup before a connection is deleted
File location convention:
{integrationId}/on-events/<name>.ts
and import it from
index.ts
.
typescript
import { createOnEvent } from 'nango';
import { z } from 'zod';

export default createOnEvent({
    description: 'Validate connection credentials',
    version: '1.0.0',
    event: 'validate-connection',
    metadata: z.void(),

    exec: async (nango) => {
        // https://api-docs-url
        await nango.get({ endpoint: '/me', retries: 3 });
    }
});
使用事件函数处理连接生命周期钩子:
  • validate-connection
    :连接创建时验证凭证/权限
  • post-connection-creation
    :连接创建后运行设置操作
  • pre-connection-deletion
    :连接删除前运行清理操作
文件位置约定:
{integrationId}/on-events/<name>.ts
,并在index.ts中导入。
typescript
import { createOnEvent } from 'nango';
import { z } from 'zod';

export default createOnEvent({
    description: '验证连接凭证',
    version: '1.0.0',
    event: 'validate-connection',
    metadata: z.void(),

    exec: async (nango) => {
        // https://api-docs-url
        await nango.get({ endpoint: '/me', retries: 3 });
    }
});

Sync Template (createSync)

Sync模板(createSync)

typescript
import { createSync } from 'nango';
import { z } from 'zod';

const RecordSchema = z.object({
    id: z.string(),
    name: z.union([z.string(), z.null()])
});

const sync = createSync({
    description: 'Brief single sentence',
    version: '1.0.0',
    endpoints: [{ method: 'GET', path: '/provider/records', group: 'Records' }],
    frequency: 'every hour',
    autoStart: true,
    syncType: 'full',

    models: {
        Record: RecordSchema
    },

    exec: async (nango) => {
        // Sync logic here
    }
});

export type NangoSyncLocal = Parameters<(typeof sync)['exec']>[0];
export default sync;
typescript
import { createSync } from 'nango';
import { z } from 'zod';

const RecordSchema = z.object({
    id: z.string(),
    name: z.union([z.string(), z.null()])
});

const sync = createSync({
    description: '简短单句描述',
    version: '1.0.0',
    endpoints: [{ method: 'GET', path: '/provider/records', group: 'Records' }],
    frequency: 'every hour',
    autoStart: true,
    syncType: 'full',

    models: {
        Record: RecordSchema
    },

    exec: async (nango) => {
        // 同步逻辑
    }
});

export type NangoSyncLocal = Parameters<(typeof sync)['exec']>[0];
export default sync;

Sync Deletion Detection

Sync删除检测

  • Do not use trackDeletes. It is deprecated.
  • Full syncs: call deleteRecordsFromPreviousExecutions at the end of exec after all batchSave calls.
  • Incremental syncs: if the API supports it, detect deletions and call batchDelete.
Important: deletion detection is a soft delete. Records remain in the cache but are marked as deleted in metadata.
Safety: only call deleteRecordsFromPreviousExecutions when the run successfully fetched the full dataset. Do not catch and swallow errors and still call it (false deletions).
typescript
await nango.deleteRecordsFromPreviousExecutions('Record');
  • 不要使用trackDeletes。该方法已废弃。
  • 全量同步:在exec的所有batchSave调用完成后,调用deleteRecordsFromPreviousExecutions。
  • 增量同步:如果API支持,检测删除并调用batchDelete。
注意:删除检测是软删除。记录仍保留在缓存中,但会在元数据中标记为已删除。
安全提示:仅当运行成功获取完整数据集时才调用deleteRecordsFromPreviousExecutions。不要捕获并吞掉错误后仍调用该方法(会导致误删除)。
typescript
await nango.deleteRecordsFromPreviousExecutions('Record');

Full Sync (Recommended)

全量同步(推荐)

typescript
exec: async (nango) => {
    const proxyConfig = {
        // https://api-docs-url
        endpoint: 'api/v1/records',
        paginate: { limit: 100 },
        retries: 3
    };

    for await (const batch of nango.paginate(proxyConfig)) {
        const records = batch.map((r: { id: string; name: string }) => ({
            id: r.id,
            name: r.name ?? null
        }));

        if (records.length > 0) {
            await nango.batchSave(records, 'Record');
        }
    }

    await nango.deleteRecordsFromPreviousExecutions('Record');
}
typescript
exec: async (nango) => {
    const proxyConfig = {
        // https://api-docs-url
        endpoint: 'api/v1/records',
        paginate: { limit: 100 },
        retries: 3
    };

    for await (const batch of nango.paginate(proxyConfig)) {
        const records = batch.map((r: { id: string; name: string }) => ({
            id: r.id,
            name: r.name ?? null
        }));

        if (records.length > 0) {
            await nango.batchSave(records, 'Record');
        }
    }

    await nango.deleteRecordsFromPreviousExecutions('Record');
}

Incremental Sync

增量同步

typescript
const sync = createSync({
    syncType: 'incremental',
    frequency: 'every 5 minutes',

    exec: async (nango) => {
        const lastSync = nango.lastSyncDate;

        const proxyConfig = {
            // https://api-docs-url
            endpoint: '/api/records',
            params: {
                sort: 'updated',
                ...(lastSync && { since: lastSync.toISOString() })
            },
            paginate: { limit: 100 },
            retries: 3
        };

        for await (const batch of nango.paginate(proxyConfig)) {
            const records = batch.map((record: { id: string; name?: string }) => ({
                id: record.id,
                name: record.name ?? null
            }));
            await nango.batchSave(records, 'Record');
        }

        if (lastSync) {
            const deleted = await nango.get({
                // https://api-docs-url
                endpoint: '/api/records/deleted',
                params: { since: lastSync.toISOString() },
                retries: 3
            });
            if (deleted.data.length > 0) {
                await nango.batchDelete(
                    deleted.data.map((d: { id: string }) => ({ id: d.id })),
                    'Record'
                );
            }
        }
    }
});
typescript
const sync = createSync({
    syncType: 'incremental',
    frequency: 'every 5 minutes',

    exec: async (nango) => {
        const lastSync = nango.lastSyncDate;

        const proxyConfig = {
            // https://api-docs-url
            endpoint: '/api/records',
            params: {
                sort: 'updated',
                ...(lastSync && { since: lastSync.toISOString() })
            },
            paginate: { limit: 100 },
            retries: 3
        };

        for await (const batch of nango.paginate(proxyConfig)) {
            const records = batch.map((record: { id: string; name?: string }) => ({
                id: record.id,
                name: record.name ?? null
            }));
            await nango.batchSave(records, 'Record');
        }

        if (lastSync) {
            const deleted = await nango.get({
                // https://api-docs-url
                endpoint: '/api/records/deleted',
                params: { since: lastSync.toISOString() },
                retries: 3
            });
            if (deleted.data.length > 0) {
                await nango.batchDelete(
                    deleted.data.map((d: { id: string }) => ({ id: d.id })),
                    'Record'
                );
            }
        }
    }
});

Sync Metadata (When Required)

Sync元数据(按需使用)

typescript
const MetadataSchema = z.object({
    team_id: z.string()
});

const sync = createSync({
    metadata: MetadataSchema,

    exec: async (nango) => {
        const metadata = await nango.getMetadata();
        const teamId = metadata?.team_id;

        if (!teamId) {
            throw new Error('team_id is required in metadata.');
        }

        const response = await nango.get({
            // https://api-docs-url
            endpoint: `/v1/teams/${teamId}/projects`,
            retries: 3
        });
    }
});
Note: nango.getMetadata() is cached for up to 60 seconds during a sync execution. Metadata updates may not be visible until the next run.
typescript
const MetadataSchema = z.object({
    team_id: z.string()
});

const sync = createSync({
    metadata: MetadataSchema,

    exec: async (nango) => {
        const metadata = await nango.getMetadata();
        const teamId = metadata?.team_id;

        if (!teamId) {
            throw new Error('元数据中需要team_id。');
        }

        const response = await nango.get({
            // https://api-docs-url
            endpoint: `/v1/teams/${teamId}/projects`,
            retries: 3
        });
    }
});
注意:nango.getMetadata()在同步执行期间会缓存60秒。元数据更新可能要到下一次运行时才会生效。

Realtime Syncs (Webhooks)

实时同步(Webhooks)

Use webhookSubscriptions + onWebhook when the provider supports webhooks.
typescript
const sync = createSync({
    webhookSubscriptions: ['contact.propertyChange'],

    exec: async (nango) => {
        // Optional periodic polling
    },

    onWebhook: async (nango, payload) => {
        if (payload.subscriptionType === 'contact.propertyChange') {
            const updated = {
                id: payload.objectId,
                [payload.propertyName]: payload.propertyValue
            };
            await nango.batchSave([updated], 'Contact');
        }
    }
});
Optional merge strategy:
typescript
await nango.setMergingStrategy({ strategy: 'ignore_if_modified_after' }, 'Contact');
当提供商支持Webhooks时,使用webhookSubscriptions + onWebhook。
typescript
const sync = createSync({
    webhookSubscriptions: ['contact.propertyChange'],

    exec: async (nango) => {
        // 可选的定期轮询
    },

    onWebhook: async (nango, payload) => {
        if (payload.subscriptionType === 'contact.propertyChange') {
            const updated = {
                id: payload.objectId,
                [payload.propertyName]: payload.propertyValue
            };
            await nango.batchSave([updated], 'Contact');
        }
    }
});
可选合并策略:
typescript
await nango.setMergingStrategy({ strategy: 'ignore_if_modified_after' }, 'Contact');

Key SDK Methods (Sync)

关键SDK方法(Sync)

MethodPurpose
nango.paginate(config)Iterate through paginated responses
nango.batchSave(records, model)Save records to cache
nango.batchDelete(records, model)Mark as deleted (incremental)
nango.deleteRecordsFromPreviousExecutions(model)Auto-detect deletions (full)
nango.lastSyncDateLast sync timestamp (incremental)
方法用途
nango.paginate(config)遍历分页响应
nango.batchSave(records, model)将记录保存到缓存
nango.batchDelete(records, model)标记为已删除(增量同步)
nango.deleteRecordsFromPreviousExecutions(model)自动检测删除(全量同步)
nango.lastSyncDate上次同步时间戳(增量同步)

Pagination Helper (Advanced Config)

分页助手(高级配置)

Nango preconfigures pagination for some APIs. Override when needed.
Pagination types: cursor, link, offset.
typescript
const proxyConfig = {
    endpoint: '/tickets',
    paginate: {
        type: 'cursor',
        cursor_path_in_response: 'next',
        cursor_name_in_request: 'cursor',
        response_path: 'tickets',
        limit_name_in_request: 'limit',
        limit: 100
    },
    retries: 3
};

for await (const page of nango.paginate(proxyConfig)) {
    await nango.batchSave(page, 'Ticket');
}
Link pagination uses link_rel_in_response_header or link_path_in_response_body. Offset pagination uses offset_name_in_request.
Nango已为部分API预配置了分页。按需覆盖配置。
分页类型:cursor、link、offset。
typescript
const proxyConfig = {
    endpoint: '/tickets',
    paginate: {
        type: 'cursor',
        cursor_path_in_response: 'next',
        cursor_name_in_request: 'cursor',
        response_path: 'tickets',
        limit_name_in_request: 'limit',
        limit: 100
    },
    retries: 3
};

for await (const page of nango.paginate(proxyConfig)) {
    await nango.batchSave(page, 'Ticket');
}
Link分页使用link_rel_in_response_header或link_path_in_response_body。Offset分页使用offset_name_in_request。

Manual Cursor-Based Pagination (If Needed)

手动基于游标的分页(如需)

typescript
let cursor: string | undefined;
while (true) {
    const res = await nango.get({
        endpoint: '/api',
        params: { cursor },
        retries: 3
    });
    const records = res.data.items.map((item: { id: string; name?: string }) => ({
        id: item.id,
        name: item.name ?? null
    }));
    await nango.batchSave(records, 'Record');
    cursor = res.data.next_cursor;
    if (!cursor) break;
}
typescript
let cursor: string | undefined;
while (true) {
    const res = await nango.get({
        endpoint: '/api',
        params: { cursor },
        retries: 3
    });
    const records = res.data.items.map((item: { id: string; name?: string }) => ({
        id: item.id,
        name: item.name ?? null
    }));
    await nango.batchSave(records, 'Record');
    cursor = res.data.next_cursor;
    if (!cursor) break;
}

Dryrun Command Reference

Dryrun命令参考

Basic syntax (action or sync):
nango dryrun <script-name> <connection-id>
Actions: pass input:
nango dryrun <action-name> <connection-id> --input '{"key":"value"}'
基本语法(action或sync):
nango dryrun <script-name> <connection-id>
Actions:传递输入参数:
nango dryrun <action-name> <connection-id> --input '{"key":"value"}'

For actions with input: z.object({})

对于无输入的Action:z.object({})

nango dryrun <action-name> <connection-id> --input '{}'

Stub metadata (when your function calls nango.getMetadata()):
nango dryrun <script-name> <connection-id> --metadata '{"team_id":"123"}' nango dryrun <script-name> <connection-id> --metadata @fixtures/metadata.json

Save mocks for tests (implies validation; only saves if validation passes):
nango dryrun <script-name> <connection-id> --save

Notes:
- Connection ID is the second positional argument (no `--connection-id` flag).
- Use `--integration-id <integration-id>` when script names overlap across integrations.
- Common flags: `--validate`, `-e/--environment dev|prod`, `--no-interactive`, `--auto-confirm`, `--lastSyncDate "YYYY-MM-DD"`, `--variant <name>`.
- If you do not have `nango` on PATH, use `npx nango ...`.
- In CI/non-interactive runs always pass `-e dev|prod` (otherwise the CLI prompts for environment selection).
- CLI upgrade prompts can block non-interactive runs. Workaround: set `NANGO_CLI_UPGRADE_MODE=ignore`.

Common mistakes:
- Using `--connection-id` (does not exist)
- Using legacy flags like `--save-responses` or `-m` (use `--save` and `--metadata`)
- Putting integration ID as the second argument (it will be interpreted as connection ID)
nango dryrun <action-name> <connection-id> --input '{}'

模拟元数据(当函数调用nango.getMetadata()时):
nango dryrun <script-name> <connection-id> --metadata '{"team_id":"123"}' nango dryrun <script-name> <connection-id> --metadata @fixtures/metadata.json

保存模拟数据用于测试(包含验证;仅当验证通过时才保存):
nango dryrun <script-name> <connection-id> --save

注意:
- 连接ID是第二个位置参数(无`--connection-id`标志)。
- 当脚本名称在不同集成中重复时,使用`--integration-id <integration-id>`。
- 常用标志:`--validate`、`-e/--environment dev|prod`、`--no-interactive`、`--auto-confirm`、`--lastSyncDate "YYYY-MM-DD"`、`--variant <name>`。
- 如果`nango`不在PATH中,使用`npx nango ...`。
- 在CI/非交互式运行中必须传递`-e dev|prod`(否则CLI会提示选择环境)。
- CLI升级提示可能会阻塞非交互式运行。解决方法:设置`NANGO_CLI_UPGRADE_MODE=ignore`。

常见错误:
- 使用`--connection-id`(该标志不存在)
- 使用旧版标志如`--save-responses`或`-m`(使用`--save`和`--metadata`)
- 将集成ID作为第二个参数(会被解析为连接ID)

Testing and Validation Workflow

测试与验证工作流

Recommended loop while coding:
  1. Implement the function file under
    {integrationId}/actions/
    or
    {integrationId}/syncs/
    .
  2. Register it via side-effect import in
    index.ts
    .
  3. Dryrun with
    nango dryrun ... --validate
    until it passes.
Dryrun + validate:
  • Action:
    nango dryrun <action-name> <connection-id> --input '{...}' --validate
  • Sync:
    nango dryrun <sync-name> <connection-id> --validate
  • Incremental sync testing: add
    --lastSyncDate "YYYY-MM-DD"
Record mocks + generate tests:
  1. nango dryrun <script-name> <connection-id> --save
    (add
    --input
    for actions; add
    --metadata
    if the script reads metadata)
  2. nango generate:tests
    (or narrow:
    -i <integrationId>
    ,
    -s <sync-name>
    ,
    -a <action-name>
    )
  3. Run tests via
    npm test
    (Vitest) or
    npx vitest run
编码时推荐的循环:
  1. {integrationId}/actions/
    {integrationId}/syncs/
    下实现函数文件。
  2. 在index.ts中通过副作用导入注册。
  3. 使用
    nango dryrun ... --validate
    进行试运行,直到通过。
试运行+验证:
  • Action:
    nango dryrun <action-name> <connection-id> --input '{...}' --validate
  • Sync:
    nango dryrun <sync-name> <connection-id> --validate
  • 增量同步测试:添加
    --lastSyncDate "YYYY-MM-DD"
记录模拟数据+生成测试用例:
  1. nango dryrun <script-name> <connection-id> --save
    (Action需添加
    --input
    ;如果脚本读取元数据则添加
    --metadata
  2. nango generate:tests
    (或缩小范围:
    -i <integrationId>
    -s <sync-name>
    -a <action-name>
  3. 通过
    npm test
    (Vitest)或
    npx vitest run
    运行测试

Mocks and Test Files (Current Format)

模拟数据与测试文件(当前格式)

{integrationId}/tests/
|-- <script-name>.test.ts
`-- <script-name>.test.json
The
.test.json
file is generated by
nango dryrun ... --save
and contains the recorded API mocks + expected input/output.
{integrationId}/tests/
|-- <script-name>.test.ts
`-- <script-name>.test.json
.test.json
文件由
nango dryrun ... --save
生成,包含录制的API模拟数据+预期输入/输出。

Deploy (Optional)

部署(可选)

Deploy functions to an environment in your Nango account:
nango deploy dev
将函数部署到Nango账户中的环境:
nango deploy dev

Deploy only one function

仅部署一个函数

nango deploy --action <action-name> dev nango deploy --sync <sync-name> dev

Reference: https://nango.dev/docs/implementation-guides/use-cases/actions/implement-an-action
nango deploy --action <action-name> dev nango deploy --sync <sync-name> dev

参考文档:https://nango.dev/docs/implementation-guides/use-cases/actions/implement-an-action

When API Docs Do Not Render

当API文档无法渲染时

If web fetching returns incomplete docs (JS-rendered):
  • Ask the user for a sample response
  • Use existing actions/syncs in the repo as a pattern
  • Run dryrun with
    --save
    and build from the captured response
如果网页抓取返回不完整的文档(JS渲染):
  • 询问用户提供示例响应
  • 使用仓库中已有的actions/syncs作为模板
  • 使用
    --save
    运行dryrun并从捕获的响应构建函数

Common Mistakes

常见错误

MistakeImpactFix
Missing/incorrect index.ts importFunction not loadedAdd side-effect import (
import './<path>.js'
)
Using legacy dryrun flags (
--save-responses
,
-m
)
Dryrun/mocks failUse
--save
and
--metadata
Calling deleteRecordsFromPreviousExecutions after partial fetchFalse deletionsLet failures fail; only call after full successful save
trackDeletes: trueDeprecatedUse deleteRecordsFromPreviousExecutions (full) or batchDelete (incremental)
Retrying non-idempotent writes blindlyDuplicate side effectsAvoid retries or use provider idempotency keys
Using any in mappingLoses type safetyUse inline types
Using --connection-idDryrun failsUse positional connection id
错误影响修复方法
index.ts导入缺失/错误函数无法加载添加副作用导入(
import './<path>.js'
使用旧版dryrun标志(
--save-responses
-m
试运行/模拟数据失败使用
--save
--metadata
部分数据获取完成后调用deleteRecordsFromPreviousExecutions误删除让失败直接抛出;仅在完整保存成功后调用
trackDeletes: true已废弃使用deleteRecordsFromPreviousExecutions(全量)或batchDelete(增量)
盲目重试非幂等写入操作重复副作用避免重试或使用提供商的幂等键
映射时使用any类型失去类型安全使用内联类型
使用--connection-id试运行失败使用位置参数传递连接ID

Final Checklists

最终检查清单

Action:
  • Nango root verified
  • Schemas + types are clear (inline or relative imports)
  • createAction with endpoint/input/output/scopes
  • Proxy config includes API doc link and intentional retries
  • nango.ActionError
    used for expected failures
  • Registered in index.ts
  • Dryrun succeeds with --validate
  • Mocks recorded with --save (if adding tests)
  • Tests generated and npm test passes
Sync:
  • Nango root verified
  • Models map defined; record ids are strings
  • createSync with endpoints/frequency/syncType
  • paginate + batchSave in exec
  • deleteRecordsFromPreviousExecutions at end for full sync
  • Metadata handled if required
  • Registered in index.ts
  • Dryrun succeeds with --validate
  • Mocks recorded with --save (if adding tests)
  • Tests generated and npm test passes
OnEvent:
  • Nango root verified
  • createOnEvent with event + exec
  • Registered in index.ts
  • Deployed and verified by triggering the lifecycle event
Action:
  • 已验证Nango根目录
  • 模式+类型清晰(内联或相对导入)
  • 使用createAction定义,包含endpoint/input/output/scopes
  • 代理配置包含API文档链接和有意设置的重试次数
  • 预期失败使用
    nango.ActionError
  • 已在index.ts中注册
  • 试运行
    --validate
    通过
  • 已使用
    --save
    记录模拟数据(如需添加测试)
  • 已生成测试用例且
    npm test
    通过
Sync:
  • 已验证Nango根目录
  • 已定义模型映射;记录ID为字符串
  • 使用createSync定义,包含endpoints/frequency/syncType
  • exec中使用paginate + batchSave
  • 全量同步在末尾调用deleteRecordsFromPreviousExecutions
  • 按需处理元数据
  • 已在index.ts中注册
  • 试运行
    --validate
    通过
  • 已使用
    --save
    记录模拟数据(如需添加测试)
  • 已生成测试用例且
    npm test
    通过
OnEvent:
  • 已验证Nango根目录
  • 使用createOnEvent定义,包含event + exec
  • 已在index.ts中注册
  • 已部署并通过触发生命周期事件验证