n8n-impl-custom-nodes — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited n8n-impl-custom-nodes (Agent Skill) and scored it 45/100 (orange). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 1 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
A base64 string of 128+ characters appears in a documentation file. Encoded prompt injection hides the hostile instruction in base64 — invisible to keyword filters — and relies on the agent's ability to decode it at runtime. There is no normal authoring reason to embed a multi-hundred-byte base64 blob in skill docs.
*.sig, SIGNATURES) outside the documentation.Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Build, test, and publish custom n8n community nodes using TypeScript.
| Task | Command / Action |
|---|---|
| Scaffold project | Clone n8n-io/n8n-nodes-starter from GitHub |
| Install dependencies | npm install |
| Development mode | npm run dev (hot-reload at http://localhost:5678) |
| Build | npm run build (compiles to dist/) |
| Lint | npm run lint |
| Publish | npm publish (package name MUST start with n8n-nodes-) |
Need a custom node?
├── Is it a REST API wrapper with standard CRUD operations?
│ └── YES → Use DECLARATIVE style (routing in properties, NO execute() method)
├── Does it require complex data transformation or custom logic?
│ └── YES → Use PROGRAMMATIC style (execute() method with full control)
└── Does it need to call multiple APIs or combine data sources?
└── YES → Use PROGRAMMATIC stylen8n-nodes-<service-name>/
├── package.json # Node registration via n8n field (CRITICAL)
├── tsconfig.json # TypeScript config
├── credentials/
│ └── MyApi.credentials.ts # Credential definitions
├── nodes/
│ └── MyNode/
│ ├── MyNode.node.ts # Main node implementation
│ ├── MyNode.node.json # Codex metadata (optional, for search/AI)
│ └── resources/ # Resource descriptions (declarative style)
├── icons/
│ └── my-icon.svg # Node icon (SVG or PNG, max 60x60px)
└── dist/ # Compiled output (generated by build)The n8n field in package.json registers your nodes and credentials with n8n.
{
"name": "n8n-nodes-myservice",
"version": "0.1.0",
"keywords": ["n8n-community-node-package"],
"n8n": {
"n8nNodesApiVersion": 1,
"credentials": [
"dist/credentials/MyApi.credentials.js"
],
"nodes": [
"dist/nodes/MyNode/MyNode.node.js"
]
},
"files": ["dist"],
"peerDependencies": {
"n8n-workflow": "*"
}
}Rules:
keywords to ["n8n-community-node-package"] — n8n uses this to discover community nodesn8n.nodes and n8n.credentials to compiled .js files in dist/.ts source files in the n8n fieldn8n-nodes- prefix for the package namen8n-workflow as a peerDependency, NEVER as a regular dependencyEvery node MUST define a description property with these required fields:
description: INodeTypeDescription = {
displayName: 'My Node', // Human-readable name shown in UI
name: 'myNode', // Internal name (camelCase, unique)
icon: 'file:myIcon.svg', // Icon reference
group: ['transform'], // 'input' | 'output' | 'transform' | 'trigger'
version: 1, // Node version (number or number[])
description: 'Short description', // Shown in node creator panel
defaults: { name: 'My Node' }, // Default instance name
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [ // Credential requirements
{ name: 'myApi', required: true },
],
properties: [ /* INodeProperties[] */ ],
};Rules:
group to ['trigger'] and inputs to [] for trigger nodesNodeConnectionTypes.Main from n8n-workflow (NEVER hardcode 'main')defaults.name that matches displayNameUse when you need full control over execution logic.
import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
import { NodeConnectionTypes } from 'n8n-workflow';
export class MyNode implements INodeType {
description: INodeTypeDescription = { /* ... */ };
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
for (let i = 0; i < items.length; i++) {
try {
const param = this.getNodeParameter('myParam', i) as string;
const credentials = await this.getCredentials('myApi');
const response = await this.helpers.httpRequest({
method: 'GET',
url: `${credentials.baseUrl}/endpoint`,
headers: { Authorization: `Bearer ${credentials.apiKey}` },
});
returnData.push({ json: response as IDataObject });
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ json: { error: (error as Error).message }, pairedItem: { item: i } });
continue;
}
throw error;
}
}
return [returnData];
}
}Rules:
this.getInputData())i to this.getNodeParameter() — it resolves expressions per itemthis.continueOnFail() in the catch blockpairedItem: { item: i } when pushing error items[returnData] for single output, [trueItems, falseItems] for two outputsUse for REST API wrappers. n8n handles HTTP requests automatically.
export class MyApiNode implements INodeType {
description: INodeTypeDescription = {
/* ... standard fields ... */
requestDefaults: {
baseURL: 'https://api.example.com',
headers: { Accept: 'application/json' },
},
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
options: [
{
name: 'Get',
value: 'get',
routing: {
request: { method: 'GET', url: '=/items/{{$parameter.itemId}}' },
},
},
],
default: 'get',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
routing: {
send: { type: 'body', property: 'title' },
},
},
],
};
// NO execute() method — n8n handles requests via routing config
}Rules:
execute() method on declarative nodesrequestDefaults.baseURL for the API base URLrouting.request on operation options to define HTTP method and URLrouting.send on parameter properties to map values to request body/queryimport type { ICredentialType, INodeProperties, IAuthenticate } from 'n8n-workflow';
export class MyApi implements ICredentialType {
name = 'myApi'; // Must match credential name in node
displayName = 'My API';
documentationUrl = 'https://docs.example.com';
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string',
typeOptions: { password: true },
default: '',
},
];
authenticate: IAuthenticate = {
type: 'generic',
properties: {
headers: { Authorization: '=Bearer {{$credentials.apiKey}}' },
},
};
}Rules:
typeOptions: { password: true } for sensitive fields (API keys, tokens, passwords)name property with the name referenced in node credentials arrayauthenticate for declarative nodes (auto-injects auth into requests)await this.getCredentials('myApi') in execute()icons/ directory or next to the node fileicon: 'file:myIcon.svg' (relative to node file or icons dir)icon: { light: 'file:icon.svg', dark: 'file:icon.dark.svg' }icon: 'fa:clock' (limited set available)git clone https://github.com/n8n-io/n8n-nodes-starter.git n8n-nodes-myservice
cd n8n-nodes-myservice
rm -rf .git
npm installnpm run dev # Starts n8n with your node loaded, hot-reload enabledOpen http://localhost:5678 — your custom node appears in the node creator panel.
npm run build # Compiles TypeScript to dist/
npm run lint # Checks code quality (ALWAYS run before publishing)# In your node package directory:
npm link
# In your n8n installation directory:
npm link n8n-nodes-myservice
# Restart n8n to load the linked packagenpm publish # Publishes to npm registrySee references/examples.md for the complete test setup pattern.
Key points:
nock definitions in the test datanodeData output for assertionnodeExecutionOrder to verify execution flown8n-nodes-keywords includes "n8n-community-node-package"n8n.nodes and n8n.credentials point to dist/*.js filesn8n-workflow is in peerDependenciesfiles field includes ["dist"]npm run build succeeds without errorsnpm run lint passes~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.