nango-function-builder
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseNango 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文档(快速链接)
- Functions runtime SDK reference: https://nango.dev/docs/reference/functions
- Implement an action: https://nango.dev/docs/implementation-guides/use-cases/actions/implement-an-action
- Implement a sync: https://nango.dev/docs/implementation-guides/use-cases/syncs/implement-a-sync
- Implement an event handler (lifecycle hooks): https://nango.dev/docs/implementation-guides/use-cases/implement-event-handler
- Testing integrations (dryrun, --save, Vitest): https://nango.dev/docs/implementation-guides/platform/functions/testing
- Deletion detection (full vs incremental): https://nango.dev/docs/implementation-guides/use-cases/syncs/deletion-detection
- Migrate from nango.yaml (Zero YAML): https://nango.dev/docs/implementation-guides/platform/migrations/migrate-to-zero-yaml
- Functions运行时SDK参考:https://nango.dev/docs/reference/functions
- 实现Action:https://nango.dev/docs/implementation-guides/use-cases/actions/implement-an-action
- 实现Sync:https://nango.dev/docs/implementation-guides/use-cases/syncs/implement-a-sync
- 实现事件处理器(生命周期钩子):https://nango.dev/docs/implementation-guides/use-cases/implement-event-handler
- 测试集成(dryrun、--save、Vitest):https://nango.dev/docs/implementation-guides/platform/functions/testing
- 删除检测(全量 vs 增量):https://nango.dev/docs/implementation-guides/use-cases/syncs/deletion-detection
- 从nango.yaml迁移(Zero YAML):https://nango.dev/docs/implementation-guides/platform/migrations/migrate-to-zero-yaml
Workflow (recommended)
推荐工作流
- Verify this is a Zero YAML TypeScript project (no ) and you are in the Nango root (
nango.yamlexists)..nango/ - Compile as needed with (one-off).
nango compile - Create/update the function file under ,
{integrationId}/actions/, or{integrationId}/syncs/.{integrationId}/on-events/ - Register the file in (side-effect import).
index.ts - Validate with .
nango dryrun ... --validate - Record mocks with and generate tests with
nango dryrun ... --save.nango generate:tests - Run .
npm test - Deploy with .
nango deploy dev
- 验证当前是Zero YAML TypeScript项目(无),且处于Nango根目录(存在
nango.yaml)。.nango/ - 按需使用编译(一次性操作)。
nango compile - 在、
{integrationId}/actions/或{integrationId}/syncs/下创建/更新函数文件。{integrationId}/on-events/ - 在中注册文件(副作用导入)。
index.ts - 使用验证。
nango dryrun ... --validate - 使用记录模拟数据,并使用
nango dryrun ... --save生成测试用例。nango generate:tests - 运行。
npm test - 使用部署。
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 (validate-connection.ts)
on-events/ - Sync files: kebab-case (many teams use a prefix, but it's optional)
fetch- - 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)
- 事件处理器文件:在目录下使用短横线命名(validate-connection.ts)
on-events/ - Sync文件:短横线命名(很多团队使用前缀,但非强制)
fetch- - 每个文件对应一个函数(action、sync或事件钩子)
- 所有actions、syncs和事件钩子必须在index.ts中导入
Register scripts in index.ts
(required)
index.ts在index.ts
中注册脚本(必填)
index.tsUse side-effect imports only (no default/named imports). Include the extension.
.jstypescript
// 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 or the function never appears.
No entry points found in index.ts...仅使用副作用导入(无默认/命名导入)。需包含扩展名。
.jstypescript
// 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 . Define functions with
nango.yaml,createAction(), orcreateSync().createOnEvent() - Register every action/sync/on-event in via side-effect import (
index.ts) or it will not load.import './<path>.js' - You cannot install/import arbitrary third-party packages in Functions. Relative imports inside the Nango project are supported. Pre-included dependencies include ,
zod/crypto, andnode:crypto/url.node:url - Sync records must include a stable string .
id - Action outputs cannot exceed 2MB.
- 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.
deleteRecordsFromPreviousExecutions() - HTTP request retries default to . Set
0intentionally (and be careful retrying non-idempotent writes).retries
- Zero YAML TypeScript项目不使用。使用
nango.yaml、createAction()或createSync()定义函数。createOnEvent() - 每个action/sync/事件钩子必须通过副作用导入()在index.ts中注册,否则无法加载。
import './<path>.js' - 不能在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 examples for IDs, timestamps, enums, and URLs.
.describe() - Avoid ; use inline types when mapping responses.
any - Prefer static Nango endpoint paths (avoid /
:idin the exposed endpoint); pass IDs in input/params.{id} - Add an API doc link comment above each provider API call.
- Standardize list actions on /
cursor.next_cursor - For optional outputs, return only when the output schema models
null.null - Use when you need custom input validation/logging; otherwise rely on schemas +
nango.zodValidateInput().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:
- is required even for "no input" actions. Use
input.z.object({}) - Do not import as a value from
ActionError(it is a type-only export in recent versions). Thrownangousing thenew nango.ActionError(payload)exec parameter.nango - typing is optional. Only import it if you explicitly annotate a variable.
ProxyConfiguration
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模式
| Operation | Method | Config Pattern |
|---|---|---|
| Create | nango.post(config) | data: { properties: {...} } |
| Read | nango.get(config) | endpoint: |
| Update | nango.patch(config) | endpoint: |
| Delete | nango.delete(config) | endpoint: |
| List | nango.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 for idempotent GET/LIST; avoid retries for non-idempotent POST unless the API supports idempotency)
3
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: |
| 更新 | nango.patch(config) | endpoint: |
| 删除 | nango.delete(config) | endpoint: |
| 列表 | 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:
| Provider | Native Input | Native Output | Map To |
|---|---|---|---|
| Slack | cursor | response_metadata.next_cursor | cursor -> next_cursor |
| Notion | start_cursor | next_cursor | cursor -> next_cursor |
| HubSpot | after | paging.next.after | cursor -> next_cursor |
| GitHub | page | Link header | cursor -> next_cursor |
| pageToken | nextPageToken | cursor -> 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()])
});提供商映射:
| 提供商 | 原生输入 | 原生输出 | 映射为 |
|---|---|---|---|
| Slack | cursor | response_metadata.next_cursor | cursor -> next_cursor |
| Notion | start_cursor | next_cursor | cursor -> next_cursor |
| HubSpot | after | paging.next.after | cursor -> next_cursor |
| GitHub | page | Link header | cursor -> next_cursor |
| pageToken | nextPageToken | cursor -> 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:
- : verify credentials/scopes on connection creation
validate-connection - : run setup after a connection is created
post-connection-creation - : cleanup before a connection is deleted
pre-connection-deletion
File location convention: and import it from .
{integrationId}/on-events/<name>.tsindex.tstypescript
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
文件位置约定:,并在index.ts中导入。
{integrationId}/on-events/<name>.tstypescript
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)
| Method | Purpose |
|---|---|
| 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.lastSyncDate | Last 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:
- Implement the function file under or
{integrationId}/actions/.{integrationId}/syncs/ - Register it via side-effect import in .
index.ts - Dryrun with until it passes.
nango dryrun ... --validate
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:
- (add
nango dryrun <script-name> <connection-id> --savefor actions; add--inputif the script reads metadata)--metadata - (or narrow:
nango generate:tests,-i <integrationId>,-s <sync-name>)-a <action-name> - Run tests via (Vitest) or
npm testnpx vitest run
编码时推荐的循环:
- 在或
{integrationId}/actions/下实现函数文件。{integrationId}/syncs/ - 在index.ts中通过副作用导入注册。
- 使用进行试运行,直到通过。
nango dryrun ... --validate
试运行+验证:
- Action:
nango dryrun <action-name> <connection-id> --input '{...}' --validate - Sync:
nango dryrun <sync-name> <connection-id> --validate - 增量同步测试:添加
--lastSyncDate "YYYY-MM-DD"
记录模拟数据+生成测试用例:
- (Action需添加
nango dryrun <script-name> <connection-id> --save;如果脚本读取元数据则添加--input)--metadata - (或缩小范围:
nango generate:tests、-i <integrationId>、-s <sync-name>)-a <action-name> - 通过(Vitest)或
npm test运行测试npx vitest run
Mocks and Test Files (Current Format)
模拟数据与测试文件(当前格式)
{integrationId}/tests/
|-- <script-name>.test.ts
`-- <script-name>.test.jsonThe file is generated by and contains the recorded API mocks + expected input/output.
.test.jsonnango dryrun ... --save{integrationId}/tests/
|-- <script-name>.test.ts
`-- <script-name>.test.json.test.jsonnango dryrun ... --saveDeploy (Optional)
部署(可选)
Deploy functions to an environment in your Nango account:
nango deploy dev将函数部署到Nango账户中的环境:
nango deploy devDeploy 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-actionnango deploy --action <action-name> dev
nango deploy --sync <sync-name> dev
参考文档:https://nango.dev/docs/implementation-guides/use-cases/actions/implement-an-actionWhen 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 and build from the captured response
--save
如果网页抓取返回不完整的文档(JS渲染):
- 询问用户提供示例响应
- 使用仓库中已有的actions/syncs作为模板
- 使用运行dryrun并从捕获的响应构建函数
--save
Common Mistakes
常见错误
| Mistake | Impact | Fix |
|---|---|---|
| Missing/incorrect index.ts import | Function not loaded | Add side-effect import ( |
Using legacy dryrun flags ( | Dryrun/mocks fail | Use |
| Calling deleteRecordsFromPreviousExecutions after partial fetch | False deletions | Let failures fail; only call after full successful save |
| trackDeletes: true | Deprecated | Use deleteRecordsFromPreviousExecutions (full) or batchDelete (incremental) |
| Retrying non-idempotent writes blindly | Duplicate side effects | Avoid retries or use provider idempotency keys |
| Using any in mapping | Loses type safety | Use inline types |
| Using --connection-id | Dryrun fails | Use positional connection id |
| 错误 | 影响 | 修复方法 |
|---|---|---|
| index.ts导入缺失/错误 | 函数无法加载 | 添加副作用导入( |
使用旧版dryrun标志( | 试运行/模拟数据失败 | 使用 |
| 部分数据获取完成后调用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
- used for expected failures
nango.ActionError - 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中注册
- 已部署并通过触发生命周期事件验证