skills/building-nango-functions-locally/SKILL.md
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.
npx skillsauth add nangohq/skills building-nango-functions-locallyInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Build deployable Nango actions and syncs in a checked-out Nango project with the local CLI validation and test workflow.
createAction()createSync()If the task is a sync, read references/syncs.md before writing code and state one of these paths first:
updated_at, modified_since, changed-records endpoint, cursor, page token, offset/page, since_id, or webhook)Invalid sync implementations:
saveCheckpoint() without getCheckpoint()syncType: 'incremental' or nango.lastSyncDate in a new synctrackDeletesStart() / trackDeletesEnd() with a changed-only checkpoint (modified_after, updated_after, changed-records endpoint). Those requests omit unchanged rows, so trackDeletesEnd() will falsely delete them.trackDeletesStart() / trackDeletesEnd() in an incremental sync that already has explicit deleted-record eventsAction:
createAction()references/actions.md before writing codeSync:
createSync()references/syncs.md before writing codeAlways:
Action-specific:
{} for no-input actions)Sync-specific:
since_id, or composite)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.
createAction() / createSync().zod, crypto/node:crypto, and url/node:url.fs, no axios, 2 MB output limit). Use a proxy script in {integration}/proxy/ with @nangohq/node instead — see references/actions.md.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.id.checkpoint schema, call nango.getCheckpoint() first, and nango.saveCheckpoint() after each page or batch.since, updated_after, cursor, page_token, offset, page, since_id, etc.). Saving one without using it is not incremental sync.syncType: 'incremental' or nango.lastSyncDate.nango.paginate(...) + nango.batchSave(...). Avoid manual while (true) loops when cursor, link, or offset pagination fits.batchDelete() when the provider returns deletions, tombstones, or delete webhooks.deleteRecordsFromPreviousExecutions() is deprecated. For full refresh, call trackDeletesStart() before fetch/save and trackDeletesEnd() only after a successful full fetch/save.trackDeletesStart() / trackDeletesEnd() with changed-only checkpoints (modified_after, updated_after, changed-records endpoints, etc.). They omit unchanged rows, so trackDeletesEnd() would delete them.trackDeletesEnd() only in the run that finishes the full window.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..describe() examples for IDs, timestamps, enums, and URLs.any; use inline mapping types.:id / {id} in the exposed endpoint); pass IDs in input or params.cursor plus a next-cursor field in the majority casing of that API (next_cursor, nextCursor, etc.).nango.zodValidateInput() only when you need custom validation or logging; otherwise rely on schemas plus the chosen validation workflow..optional()..nullable() only when null has meaning, usually clear-on-update; add .optional() when callers may omit the field too..optional() for omitted fields, .nullable() for explicit null, .nullish() only when the provider truly does both..optional() and normalize upstream null to omission unless null matters..optional() for non-required inputs and normalized outputs; widen only when the upstream contract justifies it..nullable() over z.union([z.null(), T]) or z.union([T, z.null()]).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.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/actions.mdreferences/syncs.md--save, Vitest): https://nango.dev/docs/implementation-guides/platform/functions/testingIf web fetching returns incomplete docs (JS-rendered):
references/actions.md or references/syncs.md.no nango.yaml) and that you are in the Nango root (.nango/ exists).{integrationId}/actions/ or {integrationId}/syncs/, apply the shared schema and casing rules, then register it in index.ts.nango dryrun ... --validate -e dev --no-interactive --auto-confirm.nango dryrun ... --save, then nango generate:tests, then npm test.nango deploy dev only when requested.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:
Reference: https://nango.dev/docs/implementation-guides/platform/migrations/migrate-to-zero-yaml
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/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/
|-- index.ts
|-- hubspot/
| |-- actions/
| | `-- create-contact.ts
| `-- syncs/
| `-- fetch-contacts.ts
`-- slack/
`-- actions/
`-- post-message.ts
hubspot, slack)create-contact.ts)fetch- prefix, but it is optional)index.tsindex.ts (required)Use side-effect imports only. Include the .js extension.
// index.ts
import './github/actions/get-top-contributor.js';
import './github/syncs/fetch-issues.js';
Symptom of incorrect registration: the file compiles but you see No entry points found in index.ts... or the function never appears.
Required loop:
nango dryrun ... --validate -e dev --no-interactive --auto-confirm until it passes.--input '{...}' (use --input '{}' for no-input actions).--checkpoint '{...}' when you need to simulate a resumed run.nango dryrun ... --save -e dev --no-interactive --auto-confirm to generate <script-name>.test.json.nango generate:tests, then npm test.Examples:
# Validate an action
nango dryrun <action-name> <connection-id> --validate -e dev --no-interactive --auto-confirm --input '{"key":"value"}'
# Validate a no-input action
nango dryrun <action-name> <connection-id> --validate -e dev --no-interactive --auto-confirm --input '{}'
# Validate a sync
nango dryrun <sync-name> <connection-id> --validate -e dev --no-interactive --auto-confirm
# Validate a resumed sync with a checkpoint
nango dryrun <sync-name> <connection-id> --validate -e dev --no-interactive --auto-confirm --checkpoint '{"updated_after":"2024-01-15T00:00:00Z"}'
# Record action mocks after validation passes
nango dryrun <action-name> <connection-id> --save -e dev --no-interactive --auto-confirm --input '{"key":"value"}'
# Record sync mocks after validation passes
nango dryrun <sync-name> <connection-id> --save -e dev --no-interactive --auto-confirm
# Stub metadata when needed
nango dryrun <script-name> <connection-id> --validate -e dev --no-interactive --auto-confirm --metadata '{"team_id":"123"}'
Hard rules:
<script-name>.test.json as generated output. Never create, edit, rename, or move it.--save.*.test.json; use a Vitest test with vi.spyOn(...) for 404, 401, 429, or timeout cases.--connection-id.--integration-id <integration-id> when script names overlap across integrations.--checkpoint for new incremental syncs; --lastSyncDate is a legacy pattern.nango is not on PATH, use npx nango ....NANGO_CLI_UPGRADE_MODE=ignore if needed.Reference: https://nango.dev/docs/implementation-guides/platform/functions/testing
Deploy functions to an environment in your Nango account:
nango deploy dev
# Deploy only one function
nango deploy --action <action-name> dev
nango deploy --sync <sync-name> dev
Reference: https://nango.dev/docs/implementation-guides/use-cases/actions/implement-an-action
Action:
references/actions.md was used for the action patterncreateAction() includes endpoint, input, output, and scopes when requirednango.ActionError is used for expected failuresindex.ts--validate -e dev --no-interactive --auto-confirm --input '{...}'<action-name>.test.json was generated by nango dryrun ... --save after --validatenango generate:tests ran and npm test passesSync:
references/syncs.md was used for the sync pattern.optional() unless null mattersnango.getCheckpoint() is read at the start and nango.saveCheckpoint() runs after each page or batchsince, updated_after, cursor, page_token, offset, page, since_id, etc.)modified_after, updated_after, changed-records endpoint) do not use trackDeletesStart() / trackDeletesEnd()null correctly, and fields use passthrough casing or the API's majority casingnango.paginate() is used unless the API truly cannot fit Nango's paginatorretries: 3; no sync retry value exceeds 3 without a documented exceptionbatchDelete() 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 saveindex.ts--validate -e dev --no-interactive --auto-confirm<sync-name>.test.json was generated by nango dryrun ... --save after --validatenango generate:tests ran and npm test passesdevelopment
Copy-pasteable Nango quickstart prompt for agents. Guides brand-new Nango customers from signup through API key setup, integration setup, connection authorization, and action/sync next steps. Use for first Nango integration walkthroughs.
tools
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.
development
Migrates existing Nango TypeScript createSync implementations from nango.lastSyncDate, legacy incremental syncType, and non-resumable full refreshes to checkpoint-based syncs. Use when updating customer Nango sync code to define checkpoint schemas, call getCheckpoint/saveCheckpoint/clearCheckpoint after every batchSave (including inside paginate loops), test dryruns with --checkpoint, and fix deletion handling for checkpointed incremental or full syncs.
tools
Universal gateway for any third-party API or SaaS (Google Calendar, Gmail, Slack, Notion, Linear, HubSpot, etc.). TRIGGER on any request to read or modify data in an external product, even when no matching MCP tool is loaded.