| name | action-builder-skill |
| description | 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 |
Nango Action Builder
🚨 REQUIRED: Invoke integration-patterns-skill First
Before using this skill, you MUST invoke the integration-patterns-skill using the Skill tool.
This dependency skill contains critical shared patterns for:
- Working directory detection (git root ≠ Nango root)
- Inline schema requirements (NOT from models.ts)
?? nullfor optional fields- Explicit parameter naming (
user_idnotuser) - Type safety (inline types, not
any) - No
.default()on Zod schemas - index.ts registration requirement
- Common mistakes table
If you skip invoking it, you WILL miss critical checklist items and make mistakes.
Use Skill tool: integration-patterns-skill
🚫 STOP: nango.yaml Detection
This skill only works with TypeScript-based Nango projects (using createAction()/createSync()).
Before proceeding, check if the project uses the legacy YAML configuration:
ls nango.yaml 2>/dev/null && echo "YAML PROJECT DETECTED" || echo "OK - No nango.yaml"
If you see YAML PROJECT DETECTED:
❌ STOP. This skill cannot be used with YAML-based projects.
Tell the user:
"This project uses
nango.yaml(legacy configuration). The action-builder-skill only supports TypeScript-based projects usingcreateAction(). Please upgrade your project to the TypeScript format first. See: https://docs.nango.dev/guides/custom-integrations/setup"
Do NOT attempt to:
- Create actions in a YAML-based project
- Mix YAML and TypeScript action definitions
- Use
npx nango generate:tests(it doesn't work with YAML projects)
Overview
Actions are thin API wrappers using createAction(). This skill covers action-specific patterns only.
When to Use
- Adding new API endpoint support
- Building CRUD operations (create, read, update, delete, list)
- NOT for: Complex business logic or multi-step workflows (use syncs)
createAction() Structure
import { z } from 'zod';
import { createAction } from 'nango';
import type { ProxyConfiguration } from 'nango';
// Schemas defined inline (see integration-patterns-skill)
const InputSchema = z.object({...});
const OutputSchema = z.object({...});
const action = createAction({
description: 'Brief single sentence', // No input params here
version: '1.0.0',
endpoint: {
method: 'POST', // GET, POST, PATCH, DELETE
path: '/resource', // Static path, NO :params or {params}
group: 'ResourceGroup'
},
input: InputSchema,
output: OutputSchema,
scopes: ['required.scope'],
exec: async (nango, input): Promise<z.infer<typeof OutputSchema>> => {
const config: ProxyConfiguration = {
// https://api-docs-url
endpoint: 'api/v1/resource',
data: {...}, // For POST/PATCH
params: {...}, // For GET
retries: 3 // REQUIRED
};
const response = await nango.post(config); // or .get, .patch, .delete
return {
// Transform response to match OutputSchema
// Use ?? null for optional fields (see integration-patterns-skill)
};
}
});
export type NangoActionLocal = Parameters<(typeof action)['exec']>[0];
export default action;
CRUD Methods
| Operation | Method | Config Pattern |
|---|---|---|
| Create | nango.post(config) |
data: { properties: {...} } |
| Read | nango.get(config) |
endpoint: 'resource/${id}', params: {...} |
| Update | nango.patch(config) |
endpoint: 'resource/${id}', data: {...} |
| Delete | nango.delete(config) |
endpoint: 'resource/${id}' |
| List | nango.get(config) |
params: {...} with pagination |
Required in all configs:
retries: 3- API doc link as comment above endpoint
Optional fields pattern:
data: {
required_field: input.required,
...(input.optional && { optional_field: input.optional })
}
Standard Pagination Interface
All list actions MUST use standardized cursor/next_cursor regardless of provider's native style.
Schema Pattern
const ListInput = z.object({
cursor: z.string().optional()
.describe('Pagination cursor from previous response. Omit for first page.')
});
const ListOutput = z.object({
items: z.array(ItemSchema),
next_cursor: z.union([z.string(), z.null()]) // null = no more pages
});
Provider Mapping
| Provider | Native Input | Native Output | Map To |
|---|---|---|---|
| Slack | cursor |
response_metadata.next_cursor |
cursor → next_cursor |
| Notion | start_cursor |
next_cursor |
cursor → next_cursor |
| HubSpot | after |
paging.next.after |
cursor → next_cursor |
| GitHub | page |
Link header |
cursor → next_cursor |
pageToken |
nextPageToken |
cursor → next_cursor |
Example
exec: async (nango, input): Promise<z.infer<typeof ListOutput>> => {
const config: ProxyConfiguration = {
endpoint: 'api/items',
params: {
...(input.cursor && { cursor: input.cursor })
},
retries: 3
};
const response = await nango.get(config);
return {
items: response.data.items.map((item: { id: string; name: string }) => ({
id: item.id,
name: item.name
})),
next_cursor: response.data.next_cursor || null
};
}
Dryrun Command Syntax
Exact syntax for action dryrun:
npx nango dryrun <action-name> <connection-id> --input '<json>' --integration-id <provider>
↑ ↑ ↑ ↑
│ │ │ └── Provider name (slack, hubspot, etc.)
│ │ └── JSON string with input params
│ └── Connection ID (positional, NOT a flag)
└── Action name (positional)
Arguments breakdown:
| Position/Flag | Example | Description |
|---|---|---|
| 1st positional | get-channel-info |
Action name (kebab-case) |
| 2nd positional | action-builder |
Connection ID from user |
--input |
'{"channel_id":"C123"}' |
JSON input (single quotes outside) |
--integration-id |
slack |
Provider/integration name |
Optional flags:
--save-responses- Save API response as mock--validation- Show detailed validation errors--auto-confirm- Skip confirmation prompts
Common Dryrun Mistakes
❌ WRONG - Using --connection-id flag (doesn't exist):
npx nango dryrun get-company hubspot --connection-id abc123 --input '{}'
# Error: Integration "hubspot" does not exist
❌ WRONG - Integration name as second argument:
npx nango dryrun get-company hubspot --input '{}' --integration-id hubspot
# Error: Integration "hubspot" does not exist (hubspot is being read as connection ID)
✅ CORRECT - Connection ID is positional (2nd arg):
npx nango dryrun get-company abc123 --integration-id hubspot --input '{}'
# ↑ connection ID here (no flag!)
After Creating an Action
Follow this workflow after creating the action file:
1. Register in index.ts
// Add to index.ts
import './hubspot/actions/get-company-by-domain.js';
2. Run dryrun with --save-responses
npx nango dryrun <action-name> <connection-id> --integration-id <provider> --input '{"param":"value"}' --save-responses
This validates the action works and saves the API response for test mocks.
3. Generate tests
npx nango generate:tests -a <action-name> --integration-id <provider>
# Example:
npx nango generate:tests -a get-company-by-domain --integration-id hubspot
This creates test scaffolding in {provider}/mocks/{action-name}/.
4. Run tests
npx nango test -a <action-name> --integration-id <provider>
Complete example workflow:
# After creating hubspot/actions/get-company-by-domain.ts
# 1. Register (edit index.ts to add import)
# 2. Dryrun with saved responses
npx nango dryrun get-company-by-domain abc123 --integration-id hubspot --input '{"domain":"nango.dev"}' --save-responses
# 3. Generate tests
npx nango generate:tests -a get-company-by-domain --integration-id hubspot
# 4. Run tests
npx nango test -a get-company-by-domain --integration-id hubspot
Using User-Provided Values
When the user provides test values (connection ID, IDs, etc.), use them:
- Connection ID → Use in dryrun command
- Test input values (channel ID, user ID, etc.) → Use in:
input.jsonmock file--inputflag for dryrun
- API reference URL → Fetch for schema details
When API Docs Don't Render
If WebFetch returns incomplete API docs (JavaScript-rendered content):
- Use common API patterns - Most REST APIs return similar structures
- Ask the user - "Can you provide a sample API response?"
- Run dryrun first - Use
--save-responsesto capture real response, then build schema from it - Check existing actions - Look at similar actions in the codebase for patterns
Mock Directory Structure
{integrationId}/mocks/
├── meta.json # {"connection_id": "my-connection"}
├── <action-name>/
│ ├── input.json # Test input
│ ├── output.json # Expected output
│ └── meta.json # Action-level override (optional)
└── nango/<method>/proxy/<path>/
└── <hash>.json # API response from --save-responses
Action-Specific Checklist
Structure:
-
createAction()with description, version, endpoint, input/output, scopes - Return type is
Promise<z.infer<typeof OutputSchema>> -
export type NangoActionLocalandexport default action
Zod Schemas (CRITICAL):
- NO
.default()in any schema - Nango compiler doesn't support it. Handle defaults in exec function instead.
ProxyConfiguration:
-
retries: 3configured - API doc link comment above endpoint
- Uses
inputdirectly (nozodValidateInput)
Pagination (list actions only):
- Input uses
cursor: z.string().optional() - Output uses
next_cursor: z.union([z.string(), z.null()])
See integration-patterns-skill for: schema, naming, typing, path, and index.ts registration checklist items.
Action-Specific Mistakes
| Mistake | Why It Fails | Fix |
|---|---|---|
Missing retries: 3 |
Flaky network calls fail | Add to ProxyConfiguration |
| Wrong return type | Type mismatch errors | Use Promise<z.infer<typeof OutputSchema>> |
Using zodValidateInput |
Returns undefined, already validated | Use input directly |
| Provider-specific pagination | Inconsistent API | Use cursor/next_cursor standard |
| Importing mapper functions | Not self-contained | Inline transformations in exec |
For schema, naming, typing, registration mistakes → invoke integration-patterns-skill