Build Nango Functions
Build Nango action and sync implementations without choosing the execution workflow.
This skill covers the function design contract: schemas, provider calls, action outputs, sync models, checkpoints, deletion handling, metadata, retries, and runtime constraints. It intentionally does not cover local CLI validation/deploy or remote API compile/dryrun/deploy.
If the task becomes clearly local/CLI-based, use building-nango-functions-locally instead. If it becomes clearly remote/API-based, use building-nango-functions-remotely instead.
Implementation Scope
- Build or modify a Nango function implementation
- Build an action in Nango with
createAction()
- Build a sync in Nango with
createSync()
- Use the active workflow skill for compile, dryrun, test, and deploy mechanics
Sync Strategy Gate (required before writing code)
If the task is a sync, read references/syncs.md before writing code and state one of these paths first:
- Checkpoint plan:
- change source (
updated_at, modified_since, changed-records endpoint, cursor, page token, offset/page, since_id, or webhook)
- checkpoint schema
- how the checkpoint changes the provider request or resume state
- whether the request still walks the full dataset or returns changed rows only
- delete strategy
- Full refresh blocker:
- exact provider limitation from the docs or sample payloads
- why checkpoints cannot work here
Invalid sync implementations:
- full refresh because it is simpler
saveCheckpoint() without getCheckpoint()
- reading or saving a checkpoint without using it in request params or pagination state
- using
syncType: 'incremental' or nango.lastSyncDate in a new sync
- a full refresh with no
checkpoint schema, or one that is never saved after each page — the run restarts from page 1 whenever it exceeds the execution window
saveCheckpoint() guarded so it only runs when more pages remain (e.g. if (nextCursor) { await nango.saveCheckpoint(...) }). Every successful page, including the last, must call saveCheckpoint(); otherwise a one-page sync reaches clearCheckpoint() with no checkpoint row and fails with checkpoint_conflict.
- calling
trackDeletesEnd() before clearCheckpoint(), or without a preceding clearCheckpoint() at all
- using
trackDeletesStart() / trackDeletesEnd() with a changed-only checkpoint (modified_after, updated_after, changed-records endpoint). Those requests omit unchanged rows, so trackDeletesEnd() will falsely delete them.
- using
trackDeletesStart() / trackDeletesEnd() in an incremental sync that already has explicit deleted-record events
Choose the Path
Action:
- One-time request, user-triggered, built with
createAction()
- Read
references/actions.md before writing code
Sync:
- Scheduled or webhook-driven cache updates built with
createSync()
- Complete the Sync Strategy Gate first
- Read
references/syncs.md before writing code
Required Inputs (Ask User if Missing)
Always:
- Integration ID (provider name)
- Script/function name (kebab-case)
- API reference URL or sample response
- Connection ID if the active workflow will validate or dryrun the function
Action-specific:
- Use case summary
- Input parameters
- Output fields
- Metadata JSON if required
- Test input JSON if the active workflow will validate or dryrun the action (use
{} for no-input actions)
Sync-specific:
- Model name (singular, PascalCase)
- Frequency (every hour, every 5 minutes, etc.)
- Checkpoint schema (timestamp, cursor, page token, offset/page,
since_id, or composite)
- How the checkpoint changes the provider request or resume state
- Delete strategy (deleted-record endpoint/webhook, or why full refresh is required)
- If proposing a full refresh, the exact provider limitation that blocks checkpoints from the docs/sample response
- Metadata JSON if required (team_id, workspace_id)
If any required external values are missing, ask a targeted question after checking the repo and provider docs. For syncs, choose a checkpoint plus deletion strategy whenever the provider supports one. If you cannot find a viable checkpoint strategy, state exactly why before writing a full refresh.
Non-Negotiable Rules
Shared platform constraints
- Nango functions use
createAction() / createSync().
- You cannot add arbitrary packages. Use relative imports only when the chosen workflow supports them; built-ins include
zod, crypto/node:crypto, and url/node:url.
- Use the Nango HTTP API for connection lookup, credentials, and proxy calls outside function code. Do not invent CLI token or connection commands.
- Add an API doc link comment above each provider call.
- Action outputs cannot exceed 2MB.
- File uploads and downloads cannot be implemented as actions (sandboxed runtime: no
fs, no axios, 2 MB output limit). Use a proxy script in {integration}/proxy/ with @nangohq/node instead — see references/actions.md.
- HTTP retries default to
0; set retries deliberately. Treat 3 as the normal maximum; for sync provider calls, values above 3 are effectively forbidden unless docs prove they are safe and necessary. Avoid retries for non-idempotent writes unless the API supports idempotency.
- Do not set deprecated function definition routing fields: action
endpoint and sync endpoints. Trigger actions by action name through the SDK/API, and consume sync records through the records API.
Sync rules
- Sync records need a stable string
id.
- New syncs should define a
checkpoint schema, call nango.getCheckpoint() first, and nango.saveCheckpoint() after each page or batch.
- A checkpoint is valid only if it changes the request or resume state (
since, updated_after, cursor, page_token, offset, page, since_id, etc.). Saving one without using it is not incremental sync.
- New syncs must not use
syncType: 'incremental' or nango.lastSyncDate.
- Default to
nango.paginate(...) + nango.batchSave(...). Avoid manual while (true) loops when cursor, link, or offset pagination fits.
- Prefer
batchDelete() when the provider returns deletions, tombstones, or delete webhooks.
- Use full refresh only if the provider cannot return changes, deletions, or resume state, or if the dataset is tiny.
- For full refresh, cite the exact provider limitation from docs or payloads. "It is easier" is not enough.
- Full refresh syncs still need a
checkpoint schema (page/cursor/offset) covering pagination progress, not just incremental syncs. Nango syncs run inside a time-limited execution window; a full refresh with no checkpoint restarts from page 1 on every run that exceeds the window, wasting compute re-fetching the same early pages and never reaching the rest.
deleteRecordsFromPreviousExecutions() is deprecated. For full refresh, call trackDeletesStart() on every execution (safe/idempotent — it will not overwrite the start of an already-open window), then saveCheckpoint() after each page, clearCheckpoint() after the last page, and trackDeletesEnd() only after that clearCheckpoint().
- In a full refresh, call
saveCheckpoint() after every successful page, including the last, before calling clearCheckpoint(). Never guard the save with "more pages remain." If a distinct execution path creates no checkpoint at all (for example, it processes no pages), do not call clearCheckpoint() on that path; it throws checkpoint_conflict at runtime. This exception is not a substitute for saving the terminal page.
- Never combine
trackDeletesStart() / trackDeletesEnd() with changed-only checkpoints (modified_after, updated_after, changed-records endpoints, etc.). They omit unchanged rows, so trackDeletesEnd() would delete them.
- Checkpointed full refreshes are still full refreshes. Call
trackDeletesEnd() only in the run that finishes and clears the checkpoint.
- If a sync requires metadata (e.g.
team_id, workspace_id, guild_id), set autoStart: false. The sync cannot run until the caller has set the metadata, so starting it automatically would fail.
Conventions
- Match field casing to the external API. Passthrough fields keep provider casing; non-passthrough fields should use the majority casing of that API.
- Prefer explicit field names.
- Add
.describe() examples for IDs, timestamps, enums, and URLs.
- Avoid
any; use inline mapping types.
- List actions should expose
cursor plus a next-cursor field in the majority casing of that API (next_cursor, nextCursor, etc.).
- Use
nango.zodValidateInput() only when you need custom validation or logging; otherwise rely on schemas plus the chosen validation workflow.
Schema Semantics
- Default non-required inputs to
.optional().
- Use
.nullable() only when null has meaning, usually clear-on-update; add .optional() when callers may omit the field too.
- Raw provider schemas should match the provider:
.optional() for omitted fields, .nullable() for explicit null, .nullish() only when the provider truly does both.
- Final action outputs and normalized sync models should prefer
.optional() and normalize upstream null to omission unless null matters.
- Default generated schemas to
.optional() for non-required inputs and normalized outputs; widen only when the upstream contract justifies it.
- Prefer
.nullable() over z.union([z.null(), T]) or z.union([T, z.null()]).
- Return
null only when the output schema allows it.
z.object() strips unknown keys by default. For provider pass-through use z.object({}).passthrough(), z.record(z.unknown()), or z.unknown() with minimal refinements.
Field Naming and Casing Rules
- Use explicit suffixes in the API's majority casing: IDs (
user_id, userId), names (channel_name, channelName), emails (user_email, userEmail), URLs (callback_url, callbackUrl), and timestamps (created_at, createdAt).
Mapping example (API expects a different parameter name):
const InputSchema = z.object({
userId: z.string()
});
const config: ProxyConfiguration = {
endpoint: 'users.info',
params: {
user: input.userId
},
retries: 3
};
If the API is snake_case, use user_id instead. The goal is API consistency.
References
- Action patterns, CRUD examples, metadata usage, and ActionError examples:
references/actions.md
- Sync patterns, concrete checkpoint examples, delete strategies, and full refresh fallback:
references/syncs.md
Useful Nango docs (quick links)
When API Docs Do Not Render
If web fetching returns incomplete docs (JS-rendered):
- Ask the user for a sample response
- Use existing Nango actions or syncs in the workspace as a pattern when they exist
- Use the skill-specific validation or dryrun workflow until it passes
1---2name: building-nango-functions3description: Builds Nango Function implementation patterns for createAction() and createSync() without choosing a local CLI or remote API workflow. Use only when the user asks to create or update a Nango action or sync and it is unclear whether the work should happen in a checked-out project via CLI or through Nango remote APIs. Do not load when building-nango-functions-locally or building-nango-functions-remotely applies; those skills overlap with this content and add workflow-specific validation and deploy details.4---5
6# Build Nango Functions
7Build Nango action and sync implementations without choosing the execution workflow.
8
9This skill covers the function design contract: schemas, provider calls, action outputs, sync models, checkpoints, deletion handling, metadata, retries, and runtime constraints. It intentionally does not cover local CLI validation/deploy or remote API compile/dryrun/deploy.
10
11If the task becomes clearly local/CLI-based, use `building-nango-functions-locally` instead. If it becomes clearly remote/API-based, use `building-nango-functions-remotely` instead.
12
13## Implementation Scope
14- Build or modify a Nango function implementation
15- Build an action in Nango with `createAction()`
16- Build a sync in Nango with `createSync()`
17- Use the active workflow skill for compile, dryrun, test, and deploy mechanics
18
19## Sync Strategy Gate (required before writing code)
20
21If the task is a sync, read `references/syncs.md` before writing code and state one of these paths first:
22
23- Checkpoint plan:
24 - change source (`updated_at`, `modified_since`, changed-records endpoint, cursor, page token, offset/page, `since_id`, or webhook)
25 - checkpoint schema
26 - how the checkpoint changes the provider request or resume state
27 - whether the request still walks the full dataset or returns changed rows only
28 - delete strategy
29- Full refresh blocker:
30 - exact provider limitation from the docs or sample payloads
31 - why checkpoints cannot work here
32
33Invalid sync implementations:
34- full refresh because it is simpler
35- `saveCheckpoint()` without `getCheckpoint()`
36- reading or saving a checkpoint without using it in request params or pagination state
37- using `syncType: 'incremental'` or `nango.lastSyncDate` in a new sync
38- a full refresh with no `checkpoint` schema, or one that is never saved after each page — the run restarts from page 1 whenever it exceeds the execution window
39- `saveCheckpoint()` guarded so it only runs when more pages remain (e.g. `if (nextCursor) { await nango.saveCheckpoint(...) }`). Every successful page, including the last, must call `saveCheckpoint()`; otherwise a one-page sync reaches `clearCheckpoint()` with no checkpoint row and fails with `checkpoint_conflict`.
40- calling `trackDeletesEnd()` before `clearCheckpoint()`, or without a preceding `clearCheckpoint()` at all
41- using `trackDeletesStart()` / `trackDeletesEnd()` with a changed-only checkpoint (`modified_after`, `updated_after`, changed-records endpoint). Those requests omit unchanged rows, so `trackDeletesEnd()` will falsely delete them.
42- using `trackDeletesStart()` / `trackDeletesEnd()` in an incremental sync that already has explicit deleted-record events
43
44## Choose the Path
45
46Action:
47- One-time request, user-triggered, built with `createAction()`
48- Read `references/actions.md` before writing code
49
50Sync:
51- Scheduled or webhook-driven cache updates built with `createSync()`
52- Complete the Sync Strategy Gate first
53- Read `references/syncs.md` before writing code
54
55## Required Inputs (Ask User if Missing)
56
57Always:
58- Integration ID (provider name)
59- Script/function name (kebab-case)
60- API reference URL or sample response
61- Connection ID if the active workflow will validate or dryrun the function
62
63Action-specific:
64- Use case summary
65- Input parameters
66- Output fields
67- Metadata JSON if required
68- Test input JSON if the active workflow will validate or dryrun the action (use `{}` for no-input actions)
69
70Sync-specific:
71- Model name (singular, PascalCase)
72- Frequency (every hour, every 5 minutes, etc.)
73- Checkpoint schema (timestamp, cursor, page token, offset/page, `since_id`, or composite)
74- How the checkpoint changes the provider request or resume state
75- Delete strategy (deleted-record endpoint/webhook, or why full refresh is required)
76- If proposing a full refresh, the exact provider limitation that blocks checkpoints from the docs/sample response
77- Metadata JSON if required (team_id, workspace_id)
78
79If any required external values are missing, ask a targeted question after checking the repo and provider docs. For syncs, choose a checkpoint plus deletion strategy whenever the provider supports one. If you cannot find a viable checkpoint strategy, state exactly why before writing a full refresh.
80
81## Non-Negotiable Rules
82
83### Shared platform constraints
84
85- Nango functions use `createAction()` / `createSync()`.
86- You cannot add arbitrary packages. Use relative imports only when the chosen workflow supports them; built-ins include `zod`, `crypto`/`node:crypto`, and `url`/`node:url`.
87- Use the Nango HTTP API for connection lookup, credentials, and proxy calls outside function code. Do not invent CLI token or connection commands.
88- Add an API doc link comment above each provider call.
89- Action outputs cannot exceed 2MB.
90- File uploads and downloads cannot be implemented as actions (sandboxed runtime: no `fs`, no `axios`, 2 MB output limit). Use a proxy script in `{integration}/proxy/` with `@nangohq/node` instead — see `references/actions.md`.
91- HTTP retries default to `0`; set `retries` deliberately. Treat `3` as the normal maximum; for sync provider calls, values above `3` are effectively forbidden unless docs prove they are safe and necessary. Avoid retries for non-idempotent writes unless the API supports idempotency.
92- Do not set deprecated function definition routing fields: action `endpoint` and sync `endpoints`. Trigger actions by action name through the SDK/API, and consume sync records through the records API.
93
94### Sync rules
95
96- Sync records need a stable string `id`.
97- New syncs should define a `checkpoint` schema, call `nango.getCheckpoint()` first, and `nango.saveCheckpoint()` after each page or batch.
98- A checkpoint is valid only if it changes the request or resume state (`since`, `updated_after`, `cursor`, `page_token`, `offset`, `page`, `since_id`, etc.). Saving one without using it is not incremental sync.
99- New syncs must not use `syncType: 'incremental'` or `nango.lastSyncDate`.
100- Default to `nango.paginate(...)` + `nango.batchSave(...)`. Avoid manual `while (true)` loops when `cursor`, `link`, or `offset` pagination fits.
101- Prefer `batchDelete()` when the provider returns deletions, tombstones, or delete webhooks.
102- Use full refresh only if the provider cannot return changes, deletions, or resume state, or if the dataset is tiny.
103- For full refresh, cite the exact provider limitation from docs or payloads. "It is easier" is not enough.
104- Full refresh syncs still need a `checkpoint` schema (page/cursor/offset) covering pagination progress, not just incremental syncs. Nango syncs run inside a time-limited execution window; a full refresh with no checkpoint restarts from page 1 on every run that exceeds the window, wasting compute re-fetching the same early pages and never reaching the rest.
105- `deleteRecordsFromPreviousExecutions()` is deprecated. For full refresh, call `trackDeletesStart()` on every execution (safe/idempotent — it will not overwrite the start of an already-open window), then `saveCheckpoint()` after each page, `clearCheckpoint()` after the last page, and `trackDeletesEnd()` only after that `clearCheckpoint()`.
106- In a full refresh, call `saveCheckpoint()` after every successful page, including the last, before calling `clearCheckpoint()`. Never guard the save with "more pages remain." If a distinct execution path creates no checkpoint at all (for example, it processes no pages), do not call `clearCheckpoint()` on that path; it throws `checkpoint_conflict` at runtime. This exception is not a substitute for saving the terminal page.
107- Never combine `trackDeletesStart()` / `trackDeletesEnd()` with changed-only checkpoints (`modified_after`, `updated_after`, changed-records endpoints, etc.). They omit unchanged rows, so `trackDeletesEnd()` would delete them.
108- Checkpointed full refreshes are still full refreshes. Call `trackDeletesEnd()` only in the run that finishes and clears the checkpoint.
109- If a sync requires metadata (e.g. `team_id`, `workspace_id`, `guild_id`), set `autoStart: false`. The sync cannot run until the caller has set the metadata, so starting it automatically would fail.
110
111### Conventions
112
113- Match field casing to the external API. Passthrough fields keep provider casing; non-passthrough fields should use the majority casing of that API.
114- Prefer explicit field names.
115- Add `.describe()` examples for IDs, timestamps, enums, and URLs.
116- Avoid `any`; use inline mapping types.
117- List actions should expose `cursor` plus a next-cursor field in the majority casing of that API (`next_cursor`, `nextCursor`, etc.).
118- Use `nango.zodValidateInput()` only when you need custom validation or logging; otherwise rely on schemas plus the chosen validation workflow.
119
120### Schema Semantics
121
122- Default non-required inputs to `.optional()`.
123- Use `.nullable()` only when `null` has meaning, usually clear-on-update; add `.optional()` when callers may omit the field too.
124- Raw provider schemas should match the provider: `.optional()` for omitted fields, `.nullable()` for explicit `null`, `.nullish()` only when the provider truly does both.
125- Final action outputs and normalized sync models should prefer `.optional()` and normalize upstream `null` to omission unless `null` matters.
126- Default generated schemas to `.optional()` for non-required inputs and normalized outputs; widen only when the upstream contract justifies it.
127- Prefer `.nullable()` over `z.union([z.null(), T])` or `z.union([T, z.null()])`.
128- Return `null` only when the output schema allows it.
129- `z.object()` strips unknown keys by default. For provider pass-through use `z.object({}).passthrough()`, `z.record(z.unknown())`, or `z.unknown()` with minimal refinements.
130
131### Field Naming and Casing Rules
132
133- Use explicit suffixes in the API's majority casing: IDs (`user_id`, `userId`), names (`channel_name`, `channelName`), emails (`user_email`, `userEmail`), URLs (`callback_url`, `callbackUrl`), and timestamps (`created_at`, `createdAt`).
134
135Mapping example (API expects a different parameter name):
136
137```typescript
138const InputSchema = z.object({
139 userId: z.string()
140});
141
142const config: ProxyConfiguration = {
143 endpoint: 'users.info',
144 params: {
145 user: input.userId
146 },
147 retries: 3
148};
149```
150
151If the API is snake_case, use `user_id` instead. The goal is API consistency.
152
153## References
154
155- Action patterns, CRUD examples, metadata usage, and ActionError examples: `references/actions.md`
156- Sync patterns, concrete checkpoint examples, delete strategies, and full refresh fallback: `references/syncs.md`
157
158## Useful Nango docs (quick links)
159- Functions runtime SDK reference: https://nango.dev/docs/reference/functions
160- Implement an action: https://nango.dev/docs/implementation-guides/use-cases/actions/implement-an-action
161- Implement a sync: https://nango.dev/docs/implementation-guides/use-cases/syncs/implement-a-sync
162- Checkpoints: https://nango.dev/docs/implementation-guides/use-cases/syncs/checkpoints
163- Deletion detection (full vs incremental): https://nango.dev/docs/implementation-guides/use-cases/syncs/deletion-detection
164- Testing integrations (dryrun, `--save`, Vitest): https://nango.dev/docs/implementation-guides/platform/functions/testing
165- Nango HTTP API reference: https://nango.dev/docs/reference/api
166
167## When API Docs Do Not Render
168
169If web fetching returns incomplete docs (JS-rendered):
170- Ask the user for a sample response
171- Use existing Nango actions or syncs in the workspace as a pattern when they exist
172- Use the skill-specific validation or dryrun workflow until it passes