Claude-skill-registry 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.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/integration-patterns-skill" ~/.claude/skills/majiayu000-claude-skill-registry-integration-patterns-skill && rm -rf "$T"
skills/data/integration-patterns-skill/SKILL.mdNango Integration Patterns (Shared)
This skill contains patterns shared by both actions and syncs. It is invoked as a dependency by:
- action-builder-skill - For building actions
- sync-builder-skill - For building syncs
Mandatory Checklist
CRITICAL: Create TodoWrite items for EACH of these before writing any code.
Pre-Flight Checks
- Verify working directory - Run the directory check command below. Do NOT proceed until you see "IN NANGO PROJECT ROOT"
- Confirm relative paths - All file operations use paths relative to Nango root (e.g.,
)slack/actions/create-message.ts
Schema & Type Safety
- Define schemas inline - All Zod schemas at top of file, NEVER import from models.ts
- Use
for optional fields - Never use?? null?? undefined - No
on Zod schemas - Handle defaults in exec function.default() - Explicit parameter names - Use
notuser_id
,user
notchannel_idchannel - Add
with examples - For IDs, timestamps, and constrained values.describe() - Inline types for mapping - Use
not(item: { id: string }) => ...(item: any) => ...
Endpoint & Configuration
- Static endpoint paths - No dynamic segments like
or/users/:id/users/{id} - API doc link comment - Add URL comment above endpoint in exec function
-
configured - Required in all ProxyConfigurationretries: 3
Registration (CRITICAL)
- Add import to index.ts - e.g.,
- Action/sync will NOT load without this!import './hubspot/actions/get-company.js';
Working Directory Requirements
STOP - Run This Check First
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
: You MUST NOT in Nango root
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:
- Use relative paths from Nango root:
slack/actions/create-message.ts - NEVER use absolute paths or parent directory prefixes when already in Nango root
Common 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)
Directory Structure
./ # 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:
- Provider directories: lowercase (e.g.,
,hubspot/
)salesforce/ - Action files: kebab-case (e.g.,
)create-contact.ts - Sync files: kebab-case with
prefix (e.g.,fetch-
)fetch-contacts.ts - One action/sync per file
- All actions/syncs must be imported in
to be loadedindex.ts
Note: There is NO
nango.yaml configuration file in this setup.
index.ts Registration Requirement
CRITICAL: All actions and syncs MUST be imported in
to be loaded by Nango.index.ts
// 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.
Inline Schema Pattern
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:
- Self-contained: All logic in one place
- Easier to debug: No jumping between files
- No coupling: Changes don't affect other actions/syncs
- Clear data flow: Input -> transformation -> output visible in one file
Optional Fields: ?? null
Not ?? undefined
?? null?? undefinedCRITICAL: Always use
for optional fields, never ?? null
.?? 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.
No .default()
on Zod Schemas
.default()CRITICAL: Nango compiler doesn't support
. Handle defaults in exec function..default()
// 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
Explicit Parameter Naming
Parameter names must be explicit and unambiguous. A developer should immediately understand what value to provide.
Naming Rules
- IDs: Always suffix with
(e.g.,_id
,user_id
,channel_id
)contact_id - Timestamps: Use descriptive names (e.g.,
,created_at
)scheduled_time - Names: Suffix with
when expecting a name (e.g.,_name
)channel_name - Emails: Suffix with
(e.g.,_email
)user_email - URLs: Suffix with
(e.g.,_url
)callback_url
Examples
// 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 });
Mapping to API Parameters
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 } };
Parameter Descriptions with .describe()
.describe()Use
.describe() to add documentation and examples. This helps LLMs and API consumers.
Format Pattern
"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"') });
When to Add Examples
Always include examples for:
- IDs (channel, user, message, file)
- Timestamps (Unix, Slack ts format)
- Enums or constrained values
- Format-specific strings (URLs, emails)
Optional Parameters
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.')
Type Safety for API Response Mapping
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 })) };
Endpoint Path Rules
- No dynamic segments: Paths like
or/channels/:channel
are INVALID/users/{id} - Use static paths: Put dynamic values in input schema, not path
- Unique method + path: No duplicate
across actions in same integrationGET /user
// 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
API Documentation Links
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', // ... }; }
Common Mistakes
| Mistake | Why It Fails | Fix |
|---|---|---|
| Missing index.ts import | Action/sync won't be loaded | Add to index.ts |
| Importing schemas from models.ts | Not self-contained, creates coupling | Define schemas inline at top of file |
Using | Zod expects for optional fields | Use |
Using on Zod schemas | Nango compiler doesn't support it | Handle defaults in exec function |
Ambiguous param names (, ) | Unclear what value to provide | Use explicit names (, ) |
| Loses type safety | Use inline type: |
| 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 |