Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Create new ESLint rules for the @n8n/eslint-plugin-community-nodes package. Use when adding a lint rule, creating a community node lint, or working on eslint-plugin-community-nodes. Guides rule implementation, tests, docs, and plugin registration.
.claude/skills/n8n-io-n8n-create-community-node-lint-rule/SKILL.md| Model | Eval pass | Runs |
|---|---|---|
| gemini-3.6-flash | 73% | 19 |
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 56% | 0% |
| case-02 | ✗→✓ | ▲ Improved | -3% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 6% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 98% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 41% | 0% |
Guide for adding new ESLint rules to packages/@n8n/eslint-plugin-community-nodes/.
All paths below are relative to packages/@n8n/eslint-plugin-community-nodes/.
Before writing code, clarify:
.node.ts files, credential classes, both)error (must fix) or warn (should fix)?recommended configs, or exclude from recommendedWithoutN8nCloudSupport?Create src/rules/<rule-name>.ts:
typescriptimport { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { isNodeTypeClass, // or isCredentialTypeClass findClassProperty, findObjectProperty, createRule, } from '../utils/index.js'; export const YourRuleNameRule = createRule({ name: 'rule-name', meta: { type: 'problem', // or 'suggestion' docs: { description: 'One-line description of what the rule enforces', }, messages: { messageId: 'Human-readable message. Use {{placeholder}} for dynamic data.', }, fixable: 'code', // omit if not auto-fixable hasSuggestions: true, // omit if no suggestions schema: [], // add options schema if configurable }, defaultOptions: [], create(context) { return { ClassDeclaration(node) { if (!isNodeTypeClass(node)) return; const descriptionProperty = findClassProperty(node, 'description'); if (!descriptionProperty) return; const descriptionValue = descriptionProperty.value; if (descriptionValue?.type !== AST_NODE_TYPES.ObjectExpression) return; // Rule logic here — use findObjectProperty(), getLiteralValue(), etc. context.report({ node: targetNode, messageId: 'messageId', data: { /* template vars */ }, fix(fixer) { return fixer.replaceText(targetNode, 'replacement'); }, }); }, }; }, });
Naming: Export as PascalCaseRule (e.g. MissingPairedItemRule). The name field is kebab-case.
Available AST helpers — see reference.md for the full catalog of ast-utils and file-utils exports.
Create src/rules/<rule-name>.test.ts:
typescriptimport { RuleTester } from '@typescript-eslint/rule-tester'; import { YourRuleNameRule } from './rule-name.js'; const ruleTester = new RuleTester(); // Helper to generate test code — keeps test cases readable function createNodeCode(/* parameterize the varying parts */): string { return ` import type { INodeType, INodeTypeDescription } from 'n8n-workflow'; export class TestNode implements INodeType { description: INodeTypeDescription = { displayName: 'Test Node', name: 'testNode', group: ['input'], version: 1, description: 'A test node', defaults: { name: 'Test Node' }, inputs: [], outputs: [], properties: [], }; }`; } ruleTester.run('rule-name', YourRuleNameRule, { valid: [ { name: 'class that does not implement INodeType', code: '...' }, { name: 'node with correct pattern', code: createNodeCode(/* correct */) }, ], invalid: [ { name: 'descriptive case name', code: createNodeCode(/* incorrect */), errors: [{ messageId: 'messageId', data: { /* expected template vars */ } }], output: createNodeCode(/* expected after fix */), // or `output: null` if no fix }, ], });
Test guidelines:
vi.mock('../utils/file-utils.js')errors: [{ messageId, suggestions: [...] }]src/rules/index.tstypescriptimport { YourRuleNameRule } from './rule-name.js'; // Add to the rules object: export const rules = { // ... existing rules 'rule-name': YourRuleNameRule, } satisfies Record<string, AnyRuleModule>;
src/plugin.ts configsAdd to both config objects (unless the rule depends on n8n cloud features):
typescript'@n8n/community-nodes/rule-name': 'error', // or 'warn'
error for rules that catch bugs or required patternswarn for style/convention rules (like options-sorted-alphabetically)no-restricted-globals or no-restricted-imports patterns,only add to recommended (not recommendedWithoutN8nCloudSupport)
Create docs/rules/<rule-name>.md:
markdown# Description of what the rule does (`@n8n/community-nodes/rule-name`) <!-- end auto-generated rule header --> ## Rule Details Explain why this rule exists and what problem it prevents. ## Examples ### Incorrect \`\`\`typescript // code that triggers the rule \`\`\` ### Correct \`\`\`typescript // code that passes the rule \`\`\`
The header above <!-- end auto-generated rule header --> will be regenerated by pnpm build:docs. Write a reasonable first version — it gets overwritten.
Run from packages/@n8n/eslint-plugin-community-nodes/:
bashpushd packages/@n8n/eslint-plugin-community-nodes pnpm test <rule-name>.test.ts # tests pass pnpm typecheck # types are clean pnpm build # compiles pnpm build:docs # regenerates doc headers and README table pnpm lint:docs # docs match schema popd
src/rules/<rule-name>.tssrc/rules/<rule-name>.test.tssrc/rules/index.tssrc/plugin.tsdocs/rules/<rule-name>.mdpnpm build:docs| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 29,075 | 36,013 | +24% | 1 | 1 | 0% | 4,832 | 7,558 | +56% | 0 | 0 | — |
case-02 | fail→pass | 35,367 | 27,000 | -24% | 1 | 1 | 0% | 7,544 | 7,288 | -3% | 0 | 0 | — |
case-03 | pass→pass | 24,414 | 20,251 | -17% | 1 | 1 | 0% | 3,069 | 4,829 | +57% | 0 | 0 | — |
case-04 | pass→pass | 19,883 | 17,636 | -11% | 1 | 1 | 0% | 2,611 | 3,732 | +43% | 0 | 0 | — |
case-05 | pass→pass | 21,669 | 19,470 | -10% | 1 | 1 | 0% | 3,899 | 4,079 | +5% | 0 | 0 | — |
case-06 | fail→pass | 15,167 | 4,899 | -68% | 1 | 1 | 0% | 2,448 | 2,600 | +6% | 0 | 0 | — |
case-07 | fail→pass | 12,849 | 8,332 | -35% | 1 | 1 | 0% | 1,067 | 2,110 | +98% | 0 | 0 | — |
case-08 | pass→pass | 17,805 | 11,856 | -33% | 1 | 1 | 0% | 1,923 | 2,779 | +45% | 0 | 0 | — |
case-09 | pass→pass | 10,915 | 4,965 | -55% | 1 | 1 | 0% | 857 | 2,479 | +189% | 0 | 0 | — |
case-10 | pass→pass | 19,909 | 3,861 | -81% | 1 | 1 | 0% | 2,399 | 2,310 | -4% | 0 | 0 | — |
case-11 | pass→pass | 17,510 | 9,353 | -47% | 1 | 1 | 0% | 1,976 | 2,201 | +11% | 0 | 0 | — |
case-12 | fail→pass | 16,014 | 15,647 | -2% | 1 | 1 | 0% | 2,563 | 3,617 | +41% | 0 | 0 | — |
case-13 | pass→pass | 18,095 | 7,452 | -59% | 1 | 1 | 0% | 1,876 | 2,808 | +50% | 0 | 0 | — |
case-14 | fail→pass | 15,148 | 9,522 | -37% | 1 | 1 | 0% | 1,614 | 2,316 | +43% | 0 | 0 | — |
case-15 | fail→pass | 13,375 | 7,291 | -45% | 1 | 1 | 0% | 2,083 | 1,887 | -9% | 0 | 0 | — |
case-16 | pass→pass | 18,519 | 8,067 | -56% | 1 | 1 | 0% | 1,969 | 2,100 | +7% | 0 | 0 | — |
case-17 | fail→pass | 19,660 | 4,051 | -79% | 1 | 1 | 0% | 2,040 | 2,293 | +12% | 0 | 0 | — |
case-18 | fail→pass | 42,275 | 2,712 | -94% | 1 | 1 | 0% | 6,154 | 2,031 | -67% | 0 | 0 | — |
case-19 | fail→pass | 6,033 | 7,177 | +19% | 1 | 1 | 0% | 827 | 1,970 | +138% | 0 | 0 | — |
case-20 | pass→pass | 16,833 | 8,844 | -47% | 1 | 1 | 0% | 2,125 | 2,244 | +6% | 0 | 0 | — |
case-21 | fail→pass | 15,165 | 8,508 | -44% | 1 | 1 | 0% | 1,662 | 2,158 | +30% | 0 | 0 | — |
case-22 | pass→pass | 18,647 | 7,103 | -62% | 1 | 1 | 0% | 2,130 | 2,782 | +31% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +50 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.