Builds Nango Functions in a checked-out Zero YAML TypeScript Nango project using local files, index.ts registration, nango dryrun, generated tests, and optional nango deploy via CLI. Use when creating, updating, validating, testing, or deploying Nango actions or syncs locally in a repo. This content overlaps with building-nango-functions but adds CLI workflow details, so load this instead of building-nango-functions whenever a local project, local files, Nango root, CLI, dryrun, generated tests, or nango deploy workflow is indicated.
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):
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
Workflow (required)
Decide whether this is an action or a sync.
Read the matching reference file: references/actions.md or references/syncs.md.
For syncs, inspect provider docs or payloads for checkpoints and deletes, decide whether the endpoint returns full data or changed rows, and complete the Sync Strategy Gate.
Gather required inputs and external values. For connection lookup, credentials, or discovery, use the Nango HTTP API.
Confirm this is a Zero YAML TypeScript project (no nango.yaml) and that you are in the Nango root (.nango/ exists).
Create or update the function under {integrationId}/actions/ or {integrationId}/syncs/, apply the shared schema and casing rules, then register it in index.ts.
Validate with nango dryrun ... --validate -e dev --no-interactive --auto-confirm.
If validation cannot pass, stop and report the missing external state or inputs.
After validation passes, run nango dryrun ... --save, then nango generate:tests, then npm test.
Deploy with nango deploy dev only when requested.
Preconditions (Do Before Writing Code)
Confirm TypeScript Project (No nango.yaml)
This skill only supports TypeScript projects using createAction() / createSync().
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 create files until you confirm the Nango root:
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.
references/actions.md was used for the action pattern
Schemas and types are clear, and missing-value rules match the provider versus normalized contract
createAction() includes input, output, and scopes when required; deprecated endpoint is omitted
Fields use passthrough casing or the API's majority casing
Provider call includes an API doc link comment and intentional retries
nango.ActionError is used for expected failures
Registered in index.ts
Dryrun succeeds with --validate -e dev --no-interactive --auto-confirm --input '{...}'
<action-name>.test.json was generated by nango dryrun ... --save after --validate
nango generate:tests ran and npm test passes
Sync:
Nango root verified
references/syncs.md was used for the sync pattern
Models map is defined, ids are stable strings, and normalized models prefer .optional() unless null matters
Incremental was chosen first, with a checkpoint schema unless full refresh is explicitly justified from docs or payloads
nango.getCheckpoint() is read at the start and nango.saveCheckpoint() runs after each page or batch
Checkpoint data changes the provider request or resume state (since, updated_after, cursor, page_token, offset, page, since_id, etc.)
Changed-only checkpoint syncs (modified_after, updated_after, changed-records endpoint) do not use trackDeletesStart() / trackDeletesEnd()
If checkpoints were not used, the response explains exactly why no viable checkpoint strategy exists
Raw provider schemas model omitted versus null correctly, and fields use passthrough casing or the API's majority casing
nango.paginate() is used unless the API truly cannot fit Nango's paginator
Provider API calls use retries: 3; no sync retry value exceeds 3 without a documented exception
Deletion strategy matches the sync type: batchDelete() for incremental only when the provider returns explicit deletions; otherwise full-refresh fallback uses trackDeletesStart() before fetch/save and trackDeletesEnd() only after a successful full fetch plus save
Full refresh syncs have a checkpoint schema, resume pagination from it, and call saveCheckpoint() after each page so an execution-window timeout does not restart from page 1
Full refresh trackDeletesEnd() runs only after clearCheckpoint(), on the run that finishes the last page
Metadata handled if required
Registered in index.ts
Dryrun succeeds with --validate -e dev --no-interactive --auto-confirm
<sync-name>.test.json was generated by nango dryrun ... --save after --validate
nango generate:tests ran and npm test passes
1---2name: building-nango-functions-locally3description: Builds Nango Functions in a checked-out Zero YAML TypeScript Nango project using local files, index.ts registration, nango dryrun, generated tests, and optional nango deploy via CLI. Use when creating, updating, validating, testing, or deploying Nango actions or syncs locally in a repo. This content overlaps with building-nango-functions but adds CLI workflow details, so load this instead of building-nango-functions whenever a local project, local files, Nango root, CLI, dryrun, generated tests, or nango deploy workflow is indicated.4---56# Build Nango Functions Locally
7Build deployable Nango actions and syncs in a checked-out Nango project with the local CLI validation and test workflow.
89## Implementation Scope
10- Build or modify a Nango function implementation
11- Build an action in Nango with `createAction()`
12- Build a sync in Nango with `createSync()`
13- Use the active workflow skill for compile, dryrun, test, and deploy mechanics
1415## Sync Strategy Gate (required before writing code)
1617If the task is a sync, read `references/syncs.md` before writing code and state one of these paths first:
1819- Checkpoint plan:
20 - change source (`updated_at`, `modified_since`, changed-records endpoint, cursor, page token, offset/page, `since_id`, or webhook)
21 - checkpoint schema
22 - how the checkpoint changes the provider request or resume state
23 - whether the request still walks the full dataset or returns changed rows only
24 - delete strategy
25- Full refresh blocker:
26 - exact provider limitation from the docs or sample payloads
27 - why checkpoints cannot work here
2829Invalid sync implementations:
30- full refresh because it is simpler
31- `saveCheckpoint()` without `getCheckpoint()`
32- reading or saving a checkpoint without using it in request params or pagination state
33- using `syncType: 'incremental'` or `nango.lastSyncDate` in a new sync
34- 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
35- `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`.
36- calling `trackDeletesEnd()` before `clearCheckpoint()`, or without a preceding `clearCheckpoint()` at all
37- 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.
38- using `trackDeletesStart()` / `trackDeletesEnd()` in an incremental sync that already has explicit deleted-record events
3940## Choose the Path
4142Action:
43- One-time request, user-triggered, built with `createAction()`
44- Read `references/actions.md` before writing code
4546Sync:
47- Scheduled or webhook-driven cache updates built with `createSync()`
48- Complete the Sync Strategy Gate first
49- Read `references/syncs.md` before writing code
5051## Required Inputs (Ask User if Missing)
5253Always:
54- Integration ID (provider name)
55- Script/function name (kebab-case)
56- API reference URL or sample response
57- Connection ID if the active workflow will validate or dryrun the function
5859Action-specific:
60- Use case summary
61- Input parameters
62- Output fields
63- Metadata JSON if required
64- Test input JSON if the active workflow will validate or dryrun the action (use `{}` for no-input actions)
6566Sync-specific:
67- Model name (singular, PascalCase)
68- Frequency (every hour, every 5 minutes, etc.)
69- Checkpoint schema (timestamp, cursor, page token, offset/page, `since_id`, or composite)
70- How the checkpoint changes the provider request or resume state
71- Delete strategy (deleted-record endpoint/webhook, or why full refresh is required)
72- If proposing a full refresh, the exact provider limitation that blocks checkpoints from the docs/sample response
73- Metadata JSON if required (team_id, workspace_id)
7475If 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.
7677## Non-Negotiable Rules
7879### Shared platform constraints
8081- Nango functions use `createAction()` / `createSync()`.
82- 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`.
83- Use the Nango HTTP API for connection lookup, credentials, and proxy calls outside function code. Do not invent CLI token or connection commands.
84- Add an API doc link comment above each provider call.
85- Action outputs cannot exceed 2MB.
86- 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`.
87- 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.
88- 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.
8990### Sync rules
9192- Sync records need a stable string `id`.
93- New syncs should define a `checkpoint` schema, call `nango.getCheckpoint()` first, and `nango.saveCheckpoint()` after each page or batch.
94- 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.
95- New syncs must not use `syncType: 'incremental'` or `nango.lastSyncDate`.
96- Default to `nango.paginate(...)` + `nango.batchSave(...)`. Avoid manual `while (true)` loops when `cursor`, `link`, or `offset` pagination fits.
97- Prefer `batchDelete()` when the provider returns deletions, tombstones, or delete webhooks.
98- Use full refresh only if the provider cannot return changes, deletions, or resume state, or if the dataset is tiny.
99- For full refresh, cite the exact provider limitation from docs or payloads. "It is easier" is not enough.
100- 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.
101- `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()`.
102- 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.
103- 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.
104- Checkpointed full refreshes are still full refreshes. Call `trackDeletesEnd()` only in the run that finishes and clears the checkpoint.
105- 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.
106107### Conventions
108109- Match field casing to the external API. Passthrough fields keep provider casing; non-passthrough fields should use the majority casing of that API.
110- Prefer explicit field names.
111- Add `.describe()` examples for IDs, timestamps, enums, and URLs.
112- Avoid `any`; use inline mapping types.
113- List actions should expose `cursor` plus a next-cursor field in the majority casing of that API (`next_cursor`, `nextCursor`, etc.).
114- Use `nango.zodValidateInput()` only when you need custom validation or logging; otherwise rely on schemas plus the chosen validation workflow.
115116### Schema Semantics
117118- Default non-required inputs to `.optional()`.
119- Use `.nullable()` only when `null` has meaning, usually clear-on-update; add `.optional()` when callers may omit the field too.
120- Raw provider schemas should match the provider: `.optional()` for omitted fields, `.nullable()` for explicit `null`, `.nullish()` only when the provider truly does both.
121- Final action outputs and normalized sync models should prefer `.optional()` and normalize upstream `null` to omission unless `null` matters.
122- Default generated schemas to `.optional()` for non-required inputs and normalized outputs; widen only when the upstream contract justifies it.
123- Prefer `.nullable()` over `z.union([z.null(), T])` or `z.union([T, z.null()])`.
124- Return `null` only when the output schema allows it.
125- `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.
126127### Field Naming and Casing Rules
128129- 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`).
130131Mapping example (API expects a different parameter name):
132133```typescript
134const InputSchema = z.object({
135 userId: z.string()
136});
137138const config: ProxyConfiguration = {
139 endpoint: 'users.info',
140 params: {
141 user: input.userId
142 },
143 retries: 3
144};
145```
146147If the API is snake_case, use `user_id` instead. The goal is API consistency.
148149## References
150151- Action patterns, CRUD examples, metadata usage, and ActionError examples: `references/actions.md`
152- Sync patterns, concrete checkpoint examples, delete strategies, and full refresh fallback: `references/syncs.md`
153154## Useful Nango docs (quick links)
155- Functions runtime SDK reference: https://nango.dev/docs/reference/functions
156- Implement an action: https://nango.dev/docs/implementation-guides/use-cases/actions/implement-an-action
157- Implement a sync: https://nango.dev/docs/implementation-guides/use-cases/syncs/implement-a-sync
158- Checkpoints: https://nango.dev/docs/implementation-guides/use-cases/syncs/checkpoints
159- Deletion detection (full vs incremental): https://nango.dev/docs/implementation-guides/use-cases/syncs/deletion-detection
160- Testing integrations (dryrun, `--save`, Vitest): https://nango.dev/docs/implementation-guides/platform/functions/testing
161- Nango HTTP API reference: https://nango.dev/docs/reference/api
162163## When API Docs Do Not Render
164165If web fetching returns incomplete docs (JS-rendered):
166- Ask the user for a sample response
167- Use existing Nango actions or syncs in the workspace as a pattern when they exist
168- Use the skill-specific validation or dryrun workflow until it passes
169170## Workflow (required)
1711. Decide whether this is an action or a sync.
1722. Read the matching reference file: `references/actions.md` or `references/syncs.md`.
1733. For syncs, inspect provider docs or payloads for checkpoints and deletes, decide whether the endpoint returns full data or changed rows, and complete the Sync Strategy Gate.
1744. Gather required inputs and external values. For connection lookup, credentials, or discovery, use the Nango HTTP API.
1755. Confirm this is a Zero YAML TypeScript project (`no nango.yaml`) and that you are in the Nango root (`.nango/` exists).
1766. Create or update the function under `{integrationId}/actions/` or `{integrationId}/syncs/`, apply the shared schema and casing rules, then register it in `index.ts`.
1777. Validate with `nango dryrun ... --validate -e dev --no-interactive --auto-confirm`.
1788. If validation cannot pass, stop and report the missing external state or inputs.
1799. After validation passes, run `nango dryrun ... --save`, then `nango generate:tests`, then `npm test`.
18010. Deploy with `nango deploy dev` only when requested.
181182## Preconditions (Do Before Writing Code)
183184### Confirm TypeScript Project (No `nango.yaml`)
185186This skill only supports TypeScript projects using `createAction()` / `createSync()`.
187188```bash
189ls nango.yaml 2>/dev/null && echo "YAML PROJECT DETECTED" || echo "OK - No nango.yaml"
190```
191192If you see `YAML PROJECT DETECTED`:
193- Stop immediately.
194- Tell the user to upgrade to the TypeScript format first.
195- Do not attempt to mix YAML and TypeScript.
196197Reference: https://nango.dev/docs/implementation-guides/platform/migrations/migrate-to-zero-yaml
198199### Verify Nango Project Root
200201Do not create files until you confirm the Nango root:
202203```bash
204ls -la .nango/ 2>/dev/null && pwd && echo "IN NANGO PROJECT ROOT" || echo "NOT in Nango root"
205```
206207If you see `NOT in Nango root`:
208- `cd` into the directory that contains `.nango/`
209- Re-run the check
210- Do not use absolute paths as a workaround
211212All 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.
213214## Project Structure and Naming
215216```text
217./
218|-- .nango/
219|-- index.ts
220|-- hubspot/
221| |-- actions/
222| | `-- create-contact.ts
223| `-- syncs/
224| `-- fetch-contacts.ts
225`-- slack/
226 `-- actions/
227 `-- post-message.ts
228```
229230- Provider directories: lowercase (`hubspot`, `slack`)
231- Action files: kebab-case (`create-contact.ts`)
232- Sync files: kebab-case (many teams use a `fetch-` prefix, but it is optional)
233- One function per file
234- All actions and syncs must be imported in `index.ts`
235236### Register scripts in `index.ts` (required)
237238Use side-effect imports only. Include the `.js` extension.
239240```typescript
241// index.ts
242import './github/actions/get-top-contributor.js';
243import './github/syncs/fetch-issues.js';
244```
245246Symptom of incorrect registration: the file compiles but you see `No entry points found in index.ts...` or the function never appears.
247248## Dryrun, Mocks, and Tests (required)
249250Required loop:
2511. Run `nango dryrun ... --validate -e dev --no-interactive --auto-confirm` until it passes.
2522. Actions: always pass `--input '{...}'` (use `--input '{}'` for no-input actions).
2533. Syncs: use `--checkpoint '{...}'` when you need to simulate a resumed run.
2544. If validation cannot pass, stop and state the missing external state or inputs required.
2555. After validation passes, run `nango dryrun ... --save -e dev --no-interactive --auto-confirm` to generate `<script-name>.test.json`.
2566. Run `nango generate:tests`, then `npm test`.
257258Examples:
259260```bash
261# Validate an action
262nango dryrun <action-name> <connection-id> --validate -e dev --no-interactive --auto-confirm --input '{"key":"value"}'
263264# Validate a no-input action
265nango dryrun <action-name> <connection-id> --validate -e dev --no-interactive --auto-confirm --input '{}'
266267# Validate a sync
268nango dryrun <sync-name> <connection-id> --validate -e dev --no-interactive --auto-confirm
269270# Validate a resumed sync with a checkpoint
271nango dryrun <sync-name> <connection-id> --validate -e dev --no-interactive --auto-confirm --checkpoint '{"updated_after":"2024-01-15T00:00:00Z"}'
272273# Record action mocks after validation passes
274nango dryrun <action-name> <connection-id> --save -e dev --no-interactive --auto-confirm --input '{"key":"value"}'
275276# Record sync mocks after validation passes
277nango dryrun <sync-name> <connection-id> --save -e dev --no-interactive --auto-confirm
278279# Stub metadata when needed
280nango dryrun <script-name> <connection-id> --validate -e dev --no-interactive --auto-confirm --metadata '{"team_id":"123"}'
281```
282283Hard rules:
284- Treat `<script-name>.test.json` as generated output. Never create, edit, rename, or move it.
285- If mocks are wrong or stale, fix the code and re-record with `--save`.
286- Do not hard-code error payloads in `*.test.json`; use a Vitest test with `vi.spyOn(...)` for 404, 401, 429, or timeout cases.
287- Connection ID is the second positional argument; do not use `--connection-id`.
288- Use `--integration-id <integration-id>` when script names overlap across integrations.
289- Prefer `--checkpoint` for new incremental syncs; `--lastSyncDate` is a legacy pattern.
290- If `nango` is not on `PATH`, use `npx nango ...`.
291- CLI upgrade prompts can block automation; set `NANGO_CLI_UPGRADE_MODE=ignore` if needed.
292293Reference: https://nango.dev/docs/implementation-guides/platform/functions/testing
294295## Deploy (Optional)
296297Deploy functions to an environment in your Nango account:
298299```bash
300nango deploy dev
301302# Deploy only one function
303nango deploy --action <action-name> dev
304nango deploy --sync <sync-name> dev
305```
306307Reference: https://nango.dev/docs/implementation-guides/use-cases/actions/implement-an-action
308309## Final Checklists
310311Action:
312- [ ] Nango root verified
313- [ ] `references/actions.md` was used for the action pattern
314- [ ] Schemas and types are clear, and missing-value rules match the provider versus normalized contract
315- [ ] `createAction()` includes input, output, and scopes when required; deprecated `endpoint` is omitted
316- [ ] Fields use passthrough casing or the API's majority casing
317- [ ] Provider call includes an API doc link comment and intentional retries
318- [ ] `nango.ActionError` is used for expected failures
319- [ ] Registered in `index.ts`
320- [ ] Dryrun succeeds with `--validate -e dev --no-interactive --auto-confirm --input '{...}'`
321- [ ] `<action-name>.test.json` was generated by `nango dryrun ... --save` after `--validate`
322- [ ] `nango generate:tests` ran and `npm test` passes
323324Sync:
325- [ ] Nango root verified
326- [ ] `references/syncs.md` was used for the sync pattern
327- [ ] Models map is defined, ids are stable strings, and normalized models prefer `.optional()` unless `null` matters
328- [ ] Incremental was chosen first, with a checkpoint schema unless full refresh is explicitly justified from docs or payloads
329- [ ] `nango.getCheckpoint()` is read at the start and `nango.saveCheckpoint()` runs after each page or batch
330- [ ] Checkpoint data changes the provider request or resume state (`since`, `updated_after`, `cursor`, `page_token`, `offset`, `page`, `since_id`, etc.)
331- [ ] Changed-only checkpoint syncs (`modified_after`, `updated_after`, changed-records endpoint) do not use `trackDeletesStart()` / `trackDeletesEnd()`
332- [ ] If checkpoints were not used, the response explains exactly why no viable checkpoint strategy exists
333- [ ] Raw provider schemas model omitted versus `null` correctly, and fields use passthrough casing or the API's majority casing
334- [ ] `nango.paginate()` is used unless the API truly cannot fit Nango's paginator
335- [ ] Provider API calls use `retries: 3`; no sync retry value exceeds `3` without a documented exception
336- [ ] Deletion strategy matches the sync type: `batchDelete()` for incremental only when the provider returns explicit deletions; otherwise full-refresh fallback uses `trackDeletesStart()` before fetch/save and `trackDeletesEnd()` only after a successful full fetch plus save
337- [ ] Full refresh syncs have a `checkpoint` schema, resume pagination from it, and call `saveCheckpoint()` after each page so an execution-window timeout does not restart from page 1
338- [ ] Full refresh `trackDeletesEnd()` runs only after `clearCheckpoint()`, on the run that finishes the last page
339- [ ] Metadata handled if required
340- [ ] Registered in `index.ts`
341- [ ] Dryrun succeeds with `--validate -e dev --no-interactive --auto-confirm`
342- [ ] `<sync-name>.test.json` was generated by `nango dryrun ... --save` after `--validate`
343- [ ] `nango generate:tests` ran and `npm test` passes
Run npx skillmds add nangohq/building-nango-functions-locally in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Builds Nango Functions in a checked-out Zero YAML TypeScript Nango project using local files, index.ts registration, nango dryrun, generated tests, and optional nango deploy via CLI. Use when creating, updating, validating, testing, or deploying Nango actions or syncs locally in a repo. This content overlaps with building-nango-functions but adds CLI workflow details, so load this instead of building-nango-functions whenever a local project, local files, Nango root, CLI, dryrun, generated tests, or nango deploy workflow is indicated. It is listed under DevOps & Infra on SkillMD.
This skill has not completed SkillMD's automated safety review yet. Capability flags: makes network calls, reads secrets. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
nangohq (@nangohq) published this skill. Their other Agent Skills are listed on their SkillMD profile.