بنقرة واحدة
integration-patterns-skill
// Shared patterns for Nango actions and syncs - working directory verification, inline schemas, parameter naming, type safety, and registration requirements. Private dependency skill.
// Shared patterns for Nango actions and syncs - working directory verification, inline schemas, parameter naming, type safety, and registration requirements. Private dependency skill.
Use when creating or refactoring Nango integration actions to be thin API wrappers - provides patterns for minimal transformation logic, direct proxy calls, and standardized structure
Use when creating Nango syncs for continuous data synchronization - provides patterns for pagination, batch saving, deletion detection, and incremental updates
| name | integration-patterns-skill |
| description | Shared patterns for Nango actions and syncs - working directory verification, inline schemas, parameter naming, type safety, and registration requirements. Private dependency skill. |
This skill contains patterns shared by both actions and syncs. It is invoked as a dependency by:
CRITICAL: Create TodoWrite items for EACH of these before writing any code.
slack/actions/create-message.ts)?? null for optional fields - Never use ?? undefined.default() on Zod schemas - Handle defaults in exec functionuser_id not user, channel_id not channel.describe() with examples - For IDs, timestamps, and constrained values(item: { id: string }) => ... not (item: any) => .../users/:id or /users/{id}retries: 3 configured - Required in all ProxyConfigurationimport './hubspot/actions/get-company.js'; - Action/sync will NOT load without this!DO NOT create any files until you have run this command and verified the output:
ls -la .nango/ 2>/dev/null && pwd && echo "IN NANGO PROJECT ROOT" || echo "NOT in Nango root"
Expected output: You should see .nango/ contents, the current path, and IN NANGO PROJECT ROOT
If you see NOT in Nango root: You MUST cd into the directory containing .nango/ and re-run the check.
Do NOT use absolute paths as a workaround. All file operations must use relative paths from the Nango root.
This is not optional. Skipping this check or using absolute paths as a workaround causes nested directory errors that break the build.
Why this matters: The git root may NOT be the Nango root. The Nango root is wherever .nango/ lives:
/my-project/ <- Git root (.git/ here) - May or may not be Nango root
├── .git/
├── .claude/
├── .nango/ <- If .nango/ is here, THIS is the Nango root
├── package.json
├── tsconfig.json
└── slack/
Or it may be in a subdirectory:
/my-project/ <- Git root
├── .git/
├── .claude/
└── integrations/ <- Nango root (.nango/ here) - YOU MUST BE HERE
├── .nango/
├── package.json
└── slack/
Path rules once in Nango root:
slack/actions/create-message.tsCommon mistake that WILL break the build: Creating files with extra path prefixes while already inside the Nango root directory. This creates nested structures:
integrations/integrations/slack/... <- WRONG - nested structure
Instead of:
slack/... <- CORRECT (when already in Nango root)
./ # Project root (contains .nango/, package.json)
├── hubspot/ # Provider directory (lowercase)
│ ├── actions/ # Actions folder
│ │ └── create-contact.ts # Action files (kebab-case)
│ └── syncs/ # Syncs folder
│ └── fetch-contacts.ts # Sync files (kebab-case, fetch- prefix)
├── salesforce/ # Another provider
│ └── actions/
├── .nango/ # Nango configuration directory
├── index.ts # Entry point - imports all actions/syncs
├── package.json
└── tsconfig.json
Naming conventions:
hubspot/, salesforce/)create-contact.ts)fetch- prefix (e.g., fetch-contacts.ts)index.ts to be loadedNote: There is NO nango.yaml configuration file in this setup.
CRITICAL: All actions and syncs MUST be imported in index.ts to be loaded by Nango.
// index.ts
import './hubspot/actions/create-contact.js';
import './hubspot/actions/update-contact.js';
import './hubspot/syncs/fetch-contacts.js';
import './slack/actions/post-message.js';
Symptom of missing registration: Action/sync file exists, compiles without errors, but isn't included in build output (file count stays the same).
This is the #1 reason new actions/syncs don't work. Always add the import immediately after creating the file.
CRITICAL: Define schemas inline at the top of action/sync file. NEVER import from models.ts.
import { z } from 'zod';
// GOOD: Inline schema definitions
const ContactInput = z.object({
email: z.string(),
first_name: z.string().optional(),
last_name: z.string().optional()
});
const ContactOutput = z.object({
id: z.string(),
email: z.string(),
first_name: z.union([z.string(), z.null()]),
last_name: z.union([z.string(), z.null()]),
created_at: z.string()
});
// BAD: Importing from models.ts
import { ContactInput, ContactOutput } from '../models.js';
Why inline schemas:
?? null Not ?? undefinedCRITICAL: Always use ?? null for optional fields, never ?? undefined.
// GOOD
return {
id: response.data.id,
email: response.data.email,
first_name: response.data.first_name ?? null,
last_name: response.data.last_name ?? null
};
// BAD
return {
id: response.data.id,
first_name: response.data.first_name ?? undefined, // Wrong
last_name: response.data.last_name // Could be undefined
};
Why: Zod schemas expect null for optional fields. Using undefined causes validation failures.
.default() on Zod SchemasCRITICAL: Nango compiler doesn't support .default(). Handle defaults in exec function.
// DON'T: Use .default() in schema
const Input = z.object({
limit: z.number().optional().default(10) // Compilation error!
});
// DO: Handle defaults in exec function
const Input = z.object({
limit: z.number().optional()
});
// In exec function:
const limit = input.limit || 10; // Handle default here
Parameter names must be explicit and unambiguous. A developer should immediately understand what value to provide.
_id (e.g., user_id, channel_id, contact_id)created_at, scheduled_time)_name when expecting a name (e.g., channel_name)_email (e.g., user_email)_url (e.g., callback_url)// GOOD: Explicit names
const GetUserInput = z.object({
user_id: z.string() // Clear: expects a user ID
});
const RemoveFromChannelInput = z.object({
channel_id: z.string(), // Clear: expects a channel ID
user_id: z.string() // Clear: expects a user ID
});
// BAD: Ambiguous names
const GetUserInput = z.object({
user: z.string() // Is this ID, email, name, or object?
});
const RemoveFromChannelInput = z.object({
channel: z.string(), // Could be channel name or ID
user: z.string() // Ambiguous
});
When the API uses a different parameter name, map explicitly:
const GetUserInput = z.object({
user_id: z.string() // Our explicit name
});
// In exec function:
const config = {
endpoint: 'users.info',
params: {
user: input.user_id // Map to API's expected param name
}
};
.describe()Use .describe() to add documentation and examples. This helps LLMs and API consumers.
"Brief description. Example: \"value\""
const AddReactionInput = z.object({
channel_id: z.string()
.describe('The channel containing the message. Example: "C02MB5ZABA7"'),
message_ts: z.string()
.describe('Timestamp of the message. Example: "1763887648.424429"'),
reaction_name: z.string()
.describe('Emoji name without colons. Example: "thumbsup", "heart"')
});
Always include examples for:
Explain when to use:
thread_ts: z.string().optional()
.describe('Thread parent timestamp. Omit for top-level message. Example: "1763887648.424429"'),
cursor: z.string().optional()
.describe('Pagination cursor from previous response. Omit for first page.')
Use inline types for API response items. Avoid any.
// GOOD: Inline type for API response
return {
channels: response.data.channels.map((ch: { id: string; name: string; is_private: boolean }) => ({
id: ch.id,
name: ch.name,
is_private: ch.is_private
}))
};
// BAD: Using any loses type safety
return {
channels: response.data.channels.map((ch: any) => ({
id: ch.id,
name: ch.name,
is_private: ch.is_private
}))
};
/channels/:channel or /users/{id} are INVALIDGET /user across actions in same integration// BAD: Dynamic segment in path
endpoint: { method: 'GET', path: '/channels/:channel/info' }
// GOOD: Static path with input param
endpoint: { method: 'GET', path: '/channel/info' }
// Use channel_id from input in the API call
Always include API doc link as a comment above the endpoint in the exec function:
exec: async (nango, input) => {
const config = {
// https://developers.hubspot.com/docs/api/crm/contacts
endpoint: 'crm/v3/objects/contacts',
// ...
};
}
| Mistake | Why It Fails | Fix |
|---|---|---|
| Missing index.ts import | Action/sync won't be loaded | Add import './provider/actions/name.js'; to index.ts |
| Importing schemas from models.ts | Not self-contained, creates coupling | Define schemas inline at top of file |
Using ?? undefined | Zod expects null for optional fields | Use ?? null |
Using .default() on Zod schemas | Nango compiler doesn't support it | Handle defaults in exec function |
Ambiguous param names (user, channel) | Unclear what value to provide | Use explicit names (user_id, channel_id) |
(item: any) => ... | Loses type safety | Use inline type: (item: { id: string }) => ... |
| Dynamic segments in endpoint path | Invalid path format | Use static path + input params |
| Missing API doc link | Hard to verify implementation | Add comment with docs URL |
| Creating files in wrong directory | Nested paths break CLI | Verify working directory first |