Imported from alejandrosnz/n8n-nodes-trillium (
AGENTS.md). Install upstream withnpx skills add alejandrosnz/n8n-nodes-trillium. Copyright stays with the author.
AGENTS.md - Instructions for AI Coding Agents
Important: You are working with an n8n community node starter template. Follow these instructions carefully to avoid common mistakes.
🎯 Your Mission
When asked to create a new n8n node, you must:
- REPLACE the example node, don't add alongside it
- UPDATE all configuration files with the new service name
- REGENERATE documentation using README_TEMPLATE.md
- MAINTAIN the project structure and conventions
⚠️ Common Mistakes to AVOID
- ❌ Creating new nodes without removing
nodes/Example/ - ❌ Forgetting to update
package.jsonwith new node name - ❌ Not regenerating README.md from README_TEMPLATE.md
- ❌ Using generic names like "Example" in final code
- ❌ Breaking the directory structure
📋 Step-by-Step Workflow
Step 1: Understand the Request
Ask clarifying questions if needed:
- What is the service/API name?
- What operations should the node support?
- Does it need credentials? What type (API key, OAuth, etc.)?
- Any specific requirements?
Step 2: Clean Up Example Node
# Remove the example node completely
rm -rf nodes/Example/
rm -rf credentials/ExampleApi.credentials.ts
rm -rf __tests__/nodes/Example/
Step 3: Create New Node Structure
nodes/
└── [ServiceName]/
├── [ServiceName].node.ts
├── [ServiceName].node.json (optional, for declarative)
├── icon.svg
└── descriptions/
├── [Resource1]Description.ts
└── [Resource2]Description.ts
Step 4: Update package.json
CRITICAL: Update these fields in package.json:
{
"name": "n8n-nodes-[servicename]",
"version": "0.1.0",
"description": "n8n node for [Service Name]",
"keywords": ["n8n-community-node-package", "[servicename]"],
"n8n": {
"nodes": [
"dist/nodes/[ServiceName]/[ServiceName].node.js"
],
"credentials": [
"dist/credentials/[ServiceName]Api.credentials.js"
]
}
}
Step 5: Create Credentials (if needed)
File: credentials/[ServiceName]Api.credentials.ts
import type {
IAuthenticateGeneric,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class [ServiceName]Api implements ICredentialType {
name = '[serviceName]Api';
displayName = '[Service Name] API';
documentationUrl = 'https://docs.[service].com';
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string',
typeOptions: { password: true },
default: '',
required: true,
},
];
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
headers: {
Authorization: '=Bearer {{$credentials.apiKey}}',
},
},
};
test: ICredentialTestRequest = {
request: {
baseURL: 'https://api.[service].com',
url: '/v1/user', // or appropriate test endpoint
},
};
}
Step 6: Create Node Implementation
Choose the appropriate pattern:
For Simple REST APIs (Declarative Pattern)
import type {
INodeType,
INodeTypeDescription,
INodeTypeBaseDescription,
} from 'n8n-workflow';
export class [ServiceName] implements INodeType {
description: INodeTypeDescription;
constructor(baseDescription: INodeTypeBaseDescription) {
this.description = {
...baseDescription,
displayName: '[Service Name]',
name: '[serviceName]',
icon: 'file:[serviceName].svg',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Interact with [Service Name] API',
defaults: {
name: '[Service Name]',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: '[serviceName]Api',
required: true,
},
],
requestDefaults: {
baseURL: 'https://api.[service].com',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
},
properties: [
// Resource selector
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{ name: 'User', value: 'user' },
{ name: 'Post', value: 'post' },
],
default: 'user',
},
// Operation selector
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: { resource: ['user'] },
},
options: [
{ name: 'Get', value: 'get', routing: { request: { method: 'GET', url: '=/users/{{$parameter.userId}}' }}},
{ name: 'List', value: 'list', routing: { request: { method: 'GET', url: '/users' }}},
],
default: 'get',
},
// Add operation-specific parameters
],
};
}
}
For Complex Logic (Programmatic Pattern)
import type {
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
export class [ServiceName] implements INodeType {
description: INodeTypeDescription = {
displayName: '[Service Name]',
name: '[serviceName]',
// ... (same as declarative)
properties: [
// Define parameters
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
for (let i = 0; i < items.length; i++) {
try {
if (resource === 'user') {
if (operation === 'get') {
// Implementation
}
}
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ json: { error: error.message }});
continue;
}
throw error;
}
}
return [returnData];
}
}
Step 7: Write Tests
Create tests in __tests__/nodes/[ServiceName]/:
import { mock } from 'jest-mock-extended';
import type { IExecuteFunctions } from 'n8n-workflow';
import { [ServiceName] } from '../../../nodes/[ServiceName]/[ServiceName].node';
describe('[ServiceName] Node', () => {
let mockExecuteFunctions: IExecuteFunctions;
let node: [ServiceName];
beforeEach(() => {
mockExecuteFunctions = mock<IExecuteFunctions>();
node = new [ServiceName]();
});
it('should be defined', () => {
expect(node).toBeDefined();
expect(node.description.displayName).toBe('[Service Name]');
});
// Add operation tests
});
Step 8: Update README.md
IMPORTANT: Regenerate README using the template:
- Read
README_TEMPLATE.md - Replace all placeholders:
[Service Name]→ Actual service name[serviceName]→ camelCase versionn8n-nodes-starter→n8n-nodes-[servicename]
- Add actual operations and credentials info
- Include usage examples
- Save as
README.md
Step 9: Verify Changes
Run this checklist:
# Build check
npm run build
# Lint check
npm run lint
# Test check
npm test
# Verify package.json has correct:
# - name
# - description
# - n8n.nodes paths
# - n8n.credentials paths
# Verify no "Example" references remain:
grep -r "Example" nodes/ credentials/ --exclude-dir=node_modules
🎨 Node Design Guidelines
Naming Conventions
- Files:
ServiceName.node.ts(PascalCase) - Class names:
export class ServiceName(PascalCase) - Node name:
name: 'serviceName'(camelCase) - Credential name:
name: 'serviceNameApi'(camelCase + Api suffix) - Display names:
displayName: 'Service Name'(Title Case)
Icon Guidelines
- Format: SVG
- Size: Approximately 60x60px
- Monochrome or brand colors
- Simple, recognizable design
- Save as:
icon.svgin node directory
Parameter Best Practices
- Always include Resource and Operation selectors for multi-operation nodes
- Use
noDataExpression: truefor resource/operation parameters - Use
displayOptionsto show/hide fields contextually - Provide sensible defaults
- Add helpful descriptions
- Use proper parameter types (string, options, number, boolean, etc.)
Error Handling
import { NodeOperationError } from 'n8n-workflow';
// For user errors
throw new NodeOperationError(this.getNode(), 'User ID is required');
// For API errors
throw new NodeApiError(this.getNode(), error);
// Support continue on fail
if (this.continueOnFail()) {
returnData.push({ json: { error: error.message }});
continue;
}
📚 Technical Reference
For detailed technical information, refer to:
- Node Architecture: See "Node Structure" section below
- Testing Patterns:
agents/UNIT_TESTING.md - Workflow Testing:
agents/WORKFLOW_TESTING.md - Publishing:
docs/PUBLISHING.md
Node Structure Reference
Every node implements the INodeType interface with:
description: INodeTypeDescription- Node metadata and UI configurationexecute?()- For programmatic nodespoll?()- For polling triggers (setpolling: truein description)trigger?()- For generic triggerswebhook?()- For webhook triggerswebhookMethods?- Webhook lifecycle (checkExists, create, delete)methods?- loadOptions, listSearch, credentialTest, resourceMapping
Node Types
Programmatic Nodes
Use execute function for custom logic.
Declarative Nodes
Use requestDefaults and routing configuration instead of execute.
Advanced Routing for Multiple Operations
For nodes with multiple operations, use a shared routing configuration with preSend to dynamically select operation-specific routing:
routing: {
request: { method: 'GET', url: '/base' },
send: {
preSend: [
(request: IHttpRequestOptions) => {
const operation = (request.body as { operation: string }).operation;
switch (operation) {
case 'list':
return Promise.resolve({ ...request, ...ListOperation.Routing });
case 'create':
return Promise.resolve({ ...request, ...CreateOperation.Routing });
// ... other operations
default:
return Promise.resolve(request);
}
}
]
}
}
Trigger Nodes
- Webhook triggers: Implement
webhookandwebhookMethods(checkExists, create, delete). - Polling triggers: Set
polling: trueand implementpoll. UsegetWorkflowStaticData('node')to persist state. - Generic triggers: Implement
triggerfunction.
Node Parameters
Common parameter types:
string- Text inputoptions- Dropdown (static or dynamic vialoadOptionsMethod)resourceLocator- Select by list, ID, or URLcollection- Key-value pairsfixedCollection- Structured collections
Use displayOptions to show/hide fields based on other parameters. Use noDataExpression: true for resource/operation selectors.
Versioning
- Light versioning: Use version arrays in description:
version: [3, 3.1, 3.2] - Full versioning: Use
VersionedNodeTypeclass with separate version implementations.
Credentials
Credentials are defined in credentials/ directory and implement ICredentialType:
name- Internal identifierdisplayName- Human-readable nameproperties- Credential fieldsauthenticate- Authentication configuration (generic or custom function)test- Credential test request
Nodes can test credentials via methods.credentialTest.
Testing
Unit Tests
- Use
jest-mock-extendedfor mocking interfaces - Use
nockfor HTTP mocking - Mock all external dependencies
- Test happy paths, error handling, edge cases, and binary data
Best Practices
TypeScript
- Never use
anytype - use proper types orunknown - Avoid type casting with
as- use type guards instead - Define interfaces for API responses
Error Handling
- Use
NodeOperationErrorfor user-facing errors - Use
NodeApiErrorfor API-related errors - Support
continueOnFailoption when appropriate
Code Organization
- Separate operation/field descriptions into separate files
- Create reusable API request helpers in GenericFunctions
- Use marker interfaces to categorize classes (e.g.,
implements ExampleServiceN8nResource) - Use kebab-case for files, PascalCase for classes
UI/UX
- Use clear
displayNameanddescriptionfields - Set sensible default values
- Use
displayOptionsto show/hide fields conditionally
🤖 Agent Communication Protocol
When you complete a task, provide a summary like this:
✅ Task Complete: Created [Service Name] Node
Changes made:
- ✅ Removed Example node
- ✅ Created [ServiceName] node with [X] operations
- ✅ Created [ServiceName]Api credentials
- ✅ Updated package.json
- ✅ Regenerated README.md
- ✅ Added unit tests
Next steps:
1. Run `npm install`
2. Run `npm run build`
3. Test the node locally in n8n
This helps track progress and ensures nothing was missed.