@n8n/node-cli 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build.tsbuildinfo +1 -1
- package/dist/commands/release.d.ts +3 -1
- package/dist/commands/release.js +49 -3
- package/dist/commands/release.js.map +1 -1
- package/dist/template/core.js +1 -1
- package/dist/template/core.js.map +1 -1
- package/dist/template/templates/shared/default/.agents/credentials.md +115 -0
- package/dist/template/templates/shared/default/.agents/nodes-declarative.md +103 -0
- package/dist/template/templates/shared/default/.agents/nodes-programmatic.md +76 -0
- package/dist/template/templates/shared/default/.agents/nodes.md +178 -0
- package/dist/template/templates/shared/default/.agents/properties.md +174 -0
- package/dist/template/templates/shared/default/.agents/versioning.md +90 -0
- package/dist/template/templates/shared/default/.agents/workflow.md +97 -0
- package/dist/template/templates/shared/default/AGENTS.md +102 -0
- package/dist/template/templates/shared/default/CLAUDE.md +1 -0
- package/package.json +3 -3
|
@@ -2,6 +2,8 @@ import { Command } from '@oclif/core';
|
|
|
2
2
|
export default class Release extends Command {
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
|
-
static flags: {
|
|
5
|
+
static flags: {
|
|
6
|
+
publish: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
};
|
|
6
8
|
run(): Promise<void>;
|
|
7
9
|
}
|
package/dist/commands/release.js
CHANGED
|
@@ -7,10 +7,26 @@ const package_manager_1 = require("../utils/package-manager");
|
|
|
7
7
|
const prompts_2 = require("../utils/prompts");
|
|
8
8
|
class Release extends core_1.Command {
|
|
9
9
|
async run() {
|
|
10
|
-
await this.parse(Release);
|
|
10
|
+
const { flags } = await this.parse(Release);
|
|
11
11
|
(0, prompts_1.intro)(await (0, prompts_2.getCommandHeader)('n8n-node release'));
|
|
12
12
|
const pm = (await (0, package_manager_1.detectPackageManager)()) ?? 'npm';
|
|
13
|
+
const isCI = Boolean(process.env.GITHUB_ACTIONS);
|
|
13
14
|
try {
|
|
15
|
+
if (isCI) {
|
|
16
|
+
await (0, child_process_1.runCommand)(pm, ['run', 'lint'], { stdio: 'inherit' });
|
|
17
|
+
await (0, child_process_1.runCommand)(pm, ['run', 'build'], { stdio: 'inherit' });
|
|
18
|
+
await (0, child_process_1.runCommand)('npm', ['publish'], {
|
|
19
|
+
stdio: 'inherit',
|
|
20
|
+
env: {
|
|
21
|
+
RELEASE_MODE: 'true',
|
|
22
|
+
NPM_CONFIG_PROVENANCE: 'true',
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (flags.publish) {
|
|
28
|
+
prompts_1.log.warning('Publishing directly from your machine will not include npm provenance, which is required for n8n Cloud starting May 1 2026.\nConsider switching to GitHub Actions publishing. See: https://docs.n8n.io/integrations/creating-nodes/deploy/submit-community-nodes/');
|
|
29
|
+
}
|
|
14
30
|
await (0, child_process_1.runCommand)('release-it', [
|
|
15
31
|
'-n',
|
|
16
32
|
'--git.requireBranch main',
|
|
@@ -24,6 +40,7 @@ class Release extends core_1.Command {
|
|
|
24
40
|
'--github.release',
|
|
25
41
|
`--hooks.before:init="${pm} run lint && ${pm} run build"`,
|
|
26
42
|
'--hooks.after:bump="npx auto-changelog -p"',
|
|
43
|
+
...(flags.publish ? [] : ['--npm.publish=false']),
|
|
27
44
|
], {
|
|
28
45
|
stdio: 'inherit',
|
|
29
46
|
context: 'local',
|
|
@@ -31,6 +48,9 @@ class Release extends core_1.Command {
|
|
|
31
48
|
RELEASE_MODE: 'true',
|
|
32
49
|
},
|
|
33
50
|
});
|
|
51
|
+
if (!flags.publish) {
|
|
52
|
+
prompts_1.log.info('The node was not published to NPM. Starting May 1 2026, n8n requires verified community nodes to be published via GitHub Actions with npm provenance. Learn more in our documentation: https://docs.n8n.io/integrations/creating-nodes/deploy/submit-community-nodes/');
|
|
53
|
+
}
|
|
34
54
|
}
|
|
35
55
|
catch (error) {
|
|
36
56
|
if (error instanceof child_process_1.ChildProcessError) {
|
|
@@ -45,8 +65,34 @@ class Release extends core_1.Command {
|
|
|
45
65
|
}
|
|
46
66
|
}
|
|
47
67
|
}
|
|
48
|
-
Release.description =
|
|
68
|
+
Release.description = `Release your community node package.
|
|
69
|
+
|
|
70
|
+
When running locally (default): Runs release-it to bump the version interactively, generate a changelog, commit, tag, push, and create a GitHub release. Does NOT publish to npm — use GitHub Actions for that.
|
|
71
|
+
|
|
72
|
+
When running inside a GitHub Action: Detected automatically via the GITHUB_ACTIONS environment variable. Runs lint and build, then publishes with provenance enabled (NPM_CONFIG_PROVENANCE=true).
|
|
73
|
+
|
|
74
|
+
Starting May 1 2026, n8n requires all community nodes to be published via GitHub Actions with npm provenance. Provenance lets anyone cryptographically verify that a package was built from a specific repository and commit.
|
|
75
|
+
|
|
76
|
+
To set up GitHub Actions publishing:
|
|
77
|
+
1. Add a publish.yml workflow that triggers on version tags (e.g. v*.*.*).
|
|
78
|
+
2. Grant the publish job: permissions: { id-token: write, contents: read }
|
|
79
|
+
3. Use actions/setup-node with registry-url: 'https://registry.npmjs.org/'
|
|
80
|
+
4. Run \`npm run release\` as the publish step.
|
|
81
|
+
|
|
82
|
+
For npm Trusted Publishing (no long-lived secrets):
|
|
83
|
+
On npmjs.com → package settings → Trusted Publishers → add your repo and workflow name.
|
|
84
|
+
Leave NPM_TOKEN unset; GitHub's OIDC token is used automatically.
|
|
85
|
+
|
|
86
|
+
For token-based auth (fallback):
|
|
87
|
+
Add NPM_TOKEN to your repository secrets and pass it as NODE_AUTH_TOKEN.
|
|
88
|
+
|
|
89
|
+
Full documentation: https://docs.n8n.io/integrations/creating-nodes/deploy/submit-community-nodes/`;
|
|
49
90
|
Release.examples = ['<%= config.bin %> <%= command.id %>'];
|
|
50
|
-
Release.flags = {
|
|
91
|
+
Release.flags = {
|
|
92
|
+
publish: core_1.Flags.boolean({
|
|
93
|
+
description: 'Publish to npm from your local machine (not recommended). Packages published this way will not include npm provenance and cannot become verified community nodes.',
|
|
94
|
+
default: false,
|
|
95
|
+
}),
|
|
96
|
+
};
|
|
51
97
|
exports.default = Release;
|
|
52
98
|
//# sourceMappingURL=release.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"release.js","sourceRoot":"","sources":["../../src/commands/release.ts"],"names":[],"mappings":";;AAAA,
|
|
1
|
+
{"version":3,"file":"release.js","sourceRoot":"","sources":["../../src/commands/release.ts"],"names":[],"mappings":";;AAAA,4CAA4C;AAC5C,sCAA6C;AAE7C,0DAAuE;AACvE,8DAAgE;AAChE,8CAAoD;AAEpD,MAAqB,OAAQ,SAAQ,cAAO;IAkC3C,KAAK,CAAC,GAAG;QACR,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAE5C,IAAA,eAAK,EAAC,MAAM,IAAA,0BAAgB,EAAC,kBAAkB,CAAC,CAAC,CAAC;QAElD,MAAM,EAAE,GAAG,CAAC,MAAM,IAAA,sCAAoB,GAAE,CAAC,IAAI,KAAK,CAAC;QACnD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QAEjD,IAAI,CAAC;YACJ,IAAI,IAAI,EAAE,CAAC;gBACV,MAAM,IAAA,0BAAU,EAAC,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;gBAC5D,MAAM,IAAA,0BAAU,EAAC,EAAE,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;gBAC7D,MAAM,IAAA,0BAAU,EAAC,KAAK,EAAE,CAAC,SAAS,CAAC,EAAE;oBACpC,KAAK,EAAE,SAAS;oBAChB,GAAG,EAAE;wBACJ,YAAY,EAAE,MAAM;wBACpB,qBAAqB,EAAE,MAAM;qBAC7B;iBACD,CAAC,CAAC;gBACH,OAAO;YACR,CAAC;YAED,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;gBACnB,aAAG,CAAC,OAAO,CACV,mQAAmQ,CACnQ,CAAC;YACH,CAAC;YAED,MAAM,IAAA,0BAAU,EACf,YAAY,EACZ;gBACC,IAAI;gBACJ,0BAA0B;gBAC1B,8BAA8B;gBAC9B,uBAAuB;gBACvB,sBAAsB;gBACtB,cAAc;gBACd,WAAW;gBACX,YAAY;gBACZ,kGAAkG;gBAClG,kBAAkB;gBAClB,wBAAwB,EAAE,gBAAgB,EAAE,aAAa;gBACzD,4CAA4C;gBAC5C,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC;aACjD,EACD;gBACC,KAAK,EAAE,SAAS;gBAChB,OAAO,EAAE,OAAO;gBAChB,GAAG,EAAE;oBACJ,YAAY,EAAE,MAAM;iBACpB;aACD,CACD,CAAC;YAEF,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;gBACpB,aAAG,CAAC,IAAI,CACP,uQAAuQ,CACvQ,CAAC;YACH,CAAC;QACF,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,KAAK,YAAY,iCAAiB,EAAE,CAAC;gBACxC,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;oBAClB,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;gBACzC,CAAC;qBAAM,CAAC;oBACP,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC;gBAC/B,CAAC;YACF,CAAC;YACD,MAAM,KAAK,CAAC;QACb,CAAC;IACF,CAAC;;AAtGe,mBAAW,GAAG;;;;;;;;;;;;;;;;;;;;;mGAqBoE,CAAC;AAEnF,gBAAQ,GAAG,CAAC,qCAAqC,CAAC,CAAC;AAEnD,aAAK,GAAG;IACvB,OAAO,EAAE,YAAK,CAAC,OAAO,CAAC;QACtB,WAAW,EACV,mKAAmK;QACpK,OAAO,EAAE,KAAK;KACd,CAAC;CACF,CAAC;kBAhCkB,OAAO"}
|
package/dist/template/core.js
CHANGED
|
@@ -28,7 +28,7 @@ async function copyDefaultTemplateFilesToDestination(data) {
|
|
|
28
28
|
}
|
|
29
29
|
async function templateStaticFiles(data) {
|
|
30
30
|
const files = await (0, fast_glob_1.default)('**/*.{md,json,yml}', {
|
|
31
|
-
ignore: ['tsconfig.json', 'tsconfig.build.json'],
|
|
31
|
+
ignore: ['tsconfig.json', 'tsconfig.build.json', 'AGENTS.md', 'CLAUDE.md', '.agents/*.md'],
|
|
32
32
|
cwd: data.destinationPath,
|
|
33
33
|
absolute: true,
|
|
34
34
|
dot: true,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.js","sourceRoot":"","sources":["../../src/template/core.ts"],"names":[],"mappings":";;;;;AAgCA,wEASC;AAED,sFAMC;AAED,kDAkBC;AAED,wCAYC;AAnFD,0DAA6B;AAC7B,4DAAoC;AACpC,gEAAkC;AAClC,0DAA6B;AAE7B,oDAAiD;AA2B1C,KAAK,UAAU,8BAA8B,CACnD,QAA0B,EAC1B,IAAkB;IAElB,MAAM,IAAA,uBAAU,EAAC;QAChB,MAAM,EAAE,QAAQ,CAAC,IAAI;QACrB,WAAW,EAAE,IAAI,CAAC,eAAe;QACjC,MAAM,EAAE,CAAC,MAAM,EAAE,cAAc,CAAC;KAChC,CAAC,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,qCAAqC,CAAC,IAAkB;IAC7E,MAAM,IAAA,uBAAU,EAAC;QAChB,MAAM,EAAE,mBAAI,CAAC,OAAO,CAAC,SAAS,EAAE,0BAA0B,CAAC;QAC3D,WAAW,EAAE,IAAI,CAAC,eAAe;QACjC,MAAM,EAAE,CAAC,MAAM,EAAE,cAAc,CAAC;KAChC,CAAC,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,mBAAmB,CAAC,IAAkB;IAC3D,MAAM,KAAK,GAAG,MAAM,IAAA,mBAAI,EAAC,oBAAoB,EAAE;QAC9C,MAAM,EAAE,CAAC,eAAe,EAAE,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"core.js","sourceRoot":"","sources":["../../src/template/core.ts"],"names":[],"mappings":";;;;;AAgCA,wEASC;AAED,sFAMC;AAED,kDAkBC;AAED,wCAYC;AAnFD,0DAA6B;AAC7B,4DAAoC;AACpC,gEAAkC;AAClC,0DAA6B;AAE7B,oDAAiD;AA2B1C,KAAK,UAAU,8BAA8B,CACnD,QAA0B,EAC1B,IAAkB;IAElB,MAAM,IAAA,uBAAU,EAAC;QAChB,MAAM,EAAE,QAAQ,CAAC,IAAI;QACrB,WAAW,EAAE,IAAI,CAAC,eAAe;QACjC,MAAM,EAAE,CAAC,MAAM,EAAE,cAAc,CAAC;KAChC,CAAC,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,qCAAqC,CAAC,IAAkB;IAC7E,MAAM,IAAA,uBAAU,EAAC;QAChB,MAAM,EAAE,mBAAI,CAAC,OAAO,CAAC,SAAS,EAAE,0BAA0B,CAAC;QAC3D,WAAW,EAAE,IAAI,CAAC,eAAe;QACjC,MAAM,EAAE,CAAC,MAAM,EAAE,cAAc,CAAC;KAChC,CAAC,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,mBAAmB,CAAC,IAAkB;IAC3D,MAAM,KAAK,GAAG,MAAM,IAAA,mBAAI,EAAC,oBAAoB,EAAE;QAC9C,MAAM,EAAE,CAAC,eAAe,EAAE,qBAAqB,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,CAAC;QAC1F,GAAG,EAAE,IAAI,CAAC,eAAe;QACzB,QAAQ,EAAE,IAAI;QACd,GAAG,EAAE,IAAI;KACT,CAAC,CAAC;IAEH,MAAM,OAAO,CAAC,GAAG,CAChB,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;QACxB,MAAM,OAAO,GAAG,MAAM,kBAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACjD,MAAM,UAAU,GAAG,oBAAU,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;QAEzE,IAAI,UAAU,KAAK,OAAO,EAAE,CAAC;YAC5B,MAAM,kBAAE,CAAC,SAAS,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;QACtC,CAAC;IACF,CAAC,CAAC,CACF,CAAC;AACH,CAAC;AAED,SAAgB,cAAc,CAC7B,QAA0B;IAE1B,OAAO;QACN,GAAG,QAAQ;QACX,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;YACnB,MAAM,qCAAqC,CAAC,IAAI,CAAC,CAAC;YAClD,MAAM,8BAA8B,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YACrD,MAAM,mBAAmB,CAAC,IAAI,CAAC,CAAC;YAChC,MAAM,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QAC5B,CAAC;KACD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Credentials
|
|
2
|
+
|
|
3
|
+
Credentials are used to authenticate with external services and store
|
|
4
|
+
sensitive values. They are encrypted at rest.
|
|
5
|
+
|
|
6
|
+
## Credential class anatomy
|
|
7
|
+
Credentials classes have:
|
|
8
|
+
- `name` – machine name (used in nodes' `credentials` array)
|
|
9
|
+
- `displayName` – human-readable label in the UI
|
|
10
|
+
- `properties` – parameters (similar types to node properties)
|
|
11
|
+
Sensitive properties should set `typeOptions.password = true`
|
|
12
|
+
|
|
13
|
+
## Example
|
|
14
|
+
Simplified WordPress example:
|
|
15
|
+
```typescript
|
|
16
|
+
export class WordpressApi implements ICredentialType {
|
|
17
|
+
name = 'wordpressApi';
|
|
18
|
+
displayName = 'Wordpress API';
|
|
19
|
+
documentationUrl = 'wordpress';
|
|
20
|
+
|
|
21
|
+
properties: INodeProperties[] = [
|
|
22
|
+
{
|
|
23
|
+
displayName: 'Username',
|
|
24
|
+
name: 'username',
|
|
25
|
+
type: 'string',
|
|
26
|
+
default: '',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
displayName: 'Password',
|
|
30
|
+
name: 'password',
|
|
31
|
+
type: 'string',
|
|
32
|
+
typeOptions: {
|
|
33
|
+
password: true,
|
|
34
|
+
},
|
|
35
|
+
default: '',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
displayName: 'Wordpress URL',
|
|
39
|
+
name: 'url',
|
|
40
|
+
type: 'string',
|
|
41
|
+
default: '',
|
|
42
|
+
placeholder: 'https://example.com',
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
authenticate: IAuthenticateGeneric = {
|
|
47
|
+
type: 'generic',
|
|
48
|
+
properties: {
|
|
49
|
+
auth: {
|
|
50
|
+
username: '={{$credentials.username}}',
|
|
51
|
+
password: '={{$credentials.password}}',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
test: ICredentialTestRequest = {
|
|
57
|
+
request: {
|
|
58
|
+
baseURL: '={{$credentials?.url}}/wp-json/wp/v2',
|
|
59
|
+
url: '/users',
|
|
60
|
+
method: 'GET',
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Notes
|
|
67
|
+
- `test` describes how to check if credentials are valid to show a
|
|
68
|
+
message to the user in the UI
|
|
69
|
+
- Not strictly required, but strongly recommended.
|
|
70
|
+
- `authenticate` describes how to modify requests for declarative-style
|
|
71
|
+
nodes and the HTTP Request node.
|
|
72
|
+
|
|
73
|
+
## Custom authenticate function
|
|
74
|
+
You can also use a custom authenticate function:
|
|
75
|
+
```typescript
|
|
76
|
+
authenticate: IAuthenticate = async (credentials, requestOptions) => {
|
|
77
|
+
const values = (credentials.headers as { values: Array<{ name: string; value: string }> }).values;
|
|
78
|
+
|
|
79
|
+
const headers = values.reduce((acc, cur) => {
|
|
80
|
+
acc[cur.name] = cur.value;
|
|
81
|
+
return acc;
|
|
82
|
+
}, {} as Record<string, string>);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
...requestOptions,
|
|
86
|
+
headers: {
|
|
87
|
+
...requestOptions.headers,
|
|
88
|
+
...headers,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## OAuth2 credentials
|
|
95
|
+
For services using OAuth2:
|
|
96
|
+
- You usually do not need to define `test` or `authenticate` explicitly.
|
|
97
|
+
- Instead, create credentials that extend `oAuth2Api`:
|
|
98
|
+
```typescript
|
|
99
|
+
export class MyServiceOAuth2Api implements ICredentialType {
|
|
100
|
+
name = 'myServiceOAuth2Api';
|
|
101
|
+
displayName = 'My Service OAuth2 API';
|
|
102
|
+
extends = ['oAuth2Api'];
|
|
103
|
+
|
|
104
|
+
properties: INodeProperties[] = [
|
|
105
|
+
// Add only the extra properties your service needs,
|
|
106
|
+
// e.g. scopes, custom URLs, etc.
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
- The base `oAuth2Api` handles the generic OAuth2 flow.
|
|
111
|
+
- When allowing users to specify scopes in a custom OAuth2 credential,
|
|
112
|
+
make sure to follow n8n's internal rules (see n8n docs).
|
|
113
|
+
- If you want to define scopes that the credentials will request, add a
|
|
114
|
+
property with `name: 'scope'`, `type: 'hidden'` and `default` field
|
|
115
|
+
that has your desired scopes
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Declarative nodes
|
|
2
|
+
|
|
3
|
+
Preferred for most integrations that simply call external APIs.
|
|
4
|
+
|
|
5
|
+
Also read `.agents/nodes.md` for shared node anatomy and conventions.
|
|
6
|
+
|
|
7
|
+
## When to use
|
|
8
|
+
- The integration is mostly simple HTTP/REST requests and responses
|
|
9
|
+
- You can express the behavior by mapping parameters to
|
|
10
|
+
URL/query/body/headers
|
|
11
|
+
|
|
12
|
+
If you need multiple dependent API calls, complex control flow, or heavy
|
|
13
|
+
transformations, use programmatic-style instead (see
|
|
14
|
+
`.agents/nodes-programmatic.md`).
|
|
15
|
+
|
|
16
|
+
## What you define
|
|
17
|
+
- Node `properties` (parameters)
|
|
18
|
+
- `requestDefaults` (base URL, default headers)
|
|
19
|
+
- `routing` on parameters and operations:
|
|
20
|
+
- Where to send the request (URL, method)
|
|
21
|
+
- How to map parameters to query/body/headers
|
|
22
|
+
- Optional pre-send and post-receive transformations
|
|
23
|
+
|
|
24
|
+
## requestDefaults
|
|
25
|
+
```typescript
|
|
26
|
+
requestDefaults: {
|
|
27
|
+
baseURL: 'https://api.nasa.gov',
|
|
28
|
+
headers: {
|
|
29
|
+
Accept: 'application/json',
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Parameter routing
|
|
36
|
+
Example parameter with `routing`:
|
|
37
|
+
```typescript
|
|
38
|
+
{
|
|
39
|
+
displayName: 'Rover Name',
|
|
40
|
+
description: 'Choose which Mars Rover to get a photo from',
|
|
41
|
+
required: true,
|
|
42
|
+
name: 'roverName',
|
|
43
|
+
type: 'options',
|
|
44
|
+
options: [
|
|
45
|
+
{ name: 'Curiosity', value: 'curiosity' },
|
|
46
|
+
{ name: 'Opportunity', value: 'opportunity' },
|
|
47
|
+
{ name: 'Perseverance', value: 'perseverance' },
|
|
48
|
+
{ name: 'Spirit', value: 'spirit' },
|
|
49
|
+
],
|
|
50
|
+
routing: {
|
|
51
|
+
request: {
|
|
52
|
+
method: 'GET',
|
|
53
|
+
url: '=/mars-photos/api/v1/rovers/{{$value}}/photos',
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
default: 'curiosity',
|
|
57
|
+
displayOptions: {
|
|
58
|
+
show: {
|
|
59
|
+
resource: ['marsRoverPhotos'],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Post-receive transformations
|
|
66
|
+
You can modify response data with `routing.output.postReceive`:
|
|
67
|
+
```typescript
|
|
68
|
+
postReceive: [
|
|
69
|
+
{
|
|
70
|
+
type: 'setKeyValue',
|
|
71
|
+
properties: {
|
|
72
|
+
name: '={{$responseItem.modelName}}',
|
|
73
|
+
description: '={{$responseItem.modelArn}}',
|
|
74
|
+
value: '={{$responseItem.modelId}}',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: 'sort',
|
|
79
|
+
properties: {
|
|
80
|
+
key: 'name',
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
async function (
|
|
84
|
+
this: IExecuteSingleFunctions,
|
|
85
|
+
data: INodeExecutionData[],
|
|
86
|
+
response: IN8nHttpFullResponse,
|
|
87
|
+
): Promise<INodeExecutionData[]> {
|
|
88
|
+
// Custom transformation if needed
|
|
89
|
+
return data;
|
|
90
|
+
},
|
|
91
|
+
]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Pre-send transformations
|
|
95
|
+
You can also use `routing.send.preSend` to change the request before
|
|
96
|
+
sending (e.g. add custom headers or body transformations).
|
|
97
|
+
|
|
98
|
+
## Guidelines
|
|
99
|
+
- Prefer **declarative transformations** like `setKeyValue`, `sort`, etc
|
|
100
|
+
(there are other transformations that are not in the example).
|
|
101
|
+
- Only add custom functions when necessary.
|
|
102
|
+
- Declarative-style nodes only support **light versioning** (not full
|
|
103
|
+
versioning). See `.agents/versioning.md` for details.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Programmatic nodes
|
|
2
|
+
|
|
3
|
+
Programmatic-style nodes implement an `execute` method and have full
|
|
4
|
+
control over HTTP calls, loops, transformations, etc.
|
|
5
|
+
|
|
6
|
+
Also read `.agents/nodes.md` for shared node anatomy and conventions.
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
- You need multiple dependent API calls per node execution.
|
|
10
|
+
- You need complex transformations or branching logic.
|
|
11
|
+
- The API doesn't map cleanly into simple "one request per item"
|
|
12
|
+
patterns.
|
|
13
|
+
|
|
14
|
+
If the integration is mostly simple HTTP/REST requests, prefer
|
|
15
|
+
declarative-style instead (see `.agents/nodes-declarative.md`).
|
|
16
|
+
|
|
17
|
+
If you choose programmatic-style, briefly explain **why**
|
|
18
|
+
declarative-style won't work for this particular node.
|
|
19
|
+
|
|
20
|
+
## Canonical execute pattern
|
|
21
|
+
```typescript
|
|
22
|
+
async execute(
|
|
23
|
+
this: IExecuteFunctions,
|
|
24
|
+
): Promise<INodeExecutionData[][]> {
|
|
25
|
+
const items = this.getInputData();
|
|
26
|
+
const returnData: INodeExecutionData[] = [];
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < items.length; i++) {
|
|
29
|
+
try {
|
|
30
|
+
const resource = this.getNodeParameter('resource', i) as string;
|
|
31
|
+
const operation = this.getNodeParameter('operation', i) as string;
|
|
32
|
+
|
|
33
|
+
// Implement logic based on resource + operation
|
|
34
|
+
// Use this.helpers.httpRequest / httpRequestWithAuthentication, etc.
|
|
35
|
+
const responseData = {};
|
|
36
|
+
|
|
37
|
+
returnData.push({
|
|
38
|
+
json: responseData,
|
|
39
|
+
pairedItem: { item: i },
|
|
40
|
+
});
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (this.continueOnFail()) {
|
|
43
|
+
returnData.push({
|
|
44
|
+
json: {
|
|
45
|
+
error: (error as Error).message,
|
|
46
|
+
},
|
|
47
|
+
pairedItem: { item: i },
|
|
48
|
+
});
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Implement a check to see what error we have
|
|
53
|
+
const isApiError = true;
|
|
54
|
+
// Use NodeApiError for API-related errors
|
|
55
|
+
if (isApiError) {
|
|
56
|
+
throw new NodeApiError(this.getNode(), error as Error, { itemIndex: i });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Use NodeOperationError for configuration/validation errors
|
|
60
|
+
throw new NodeOperationError(this.getNode(), error as Error, { itemIndex: i });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return [returnData];
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Guidelines
|
|
69
|
+
- Always get input items via `this.getInputData()`
|
|
70
|
+
- Pass the correct item index as the second argument to
|
|
71
|
+
`getNodeParameter`
|
|
72
|
+
- Handle errors using `NodeApiError` (for API failures) and
|
|
73
|
+
`NodeOperationError` (for operational/validation errors)
|
|
74
|
+
- Support `continueOnFail()` to allow workflows to proceed when possible
|
|
75
|
+
- Programmatic-style nodes support both **light and full versioning**.
|
|
76
|
+
See `.agents/versioning.md` for details.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Building nodes
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
Community nodes depend on `n8n-workflow` package that has different interfaces,
|
|
5
|
+
classes and helper functions.
|
|
6
|
+
|
|
7
|
+
Nodes can be built using one of two styles. To identify which style an
|
|
8
|
+
existing node uses:
|
|
9
|
+
- **Declarative-style** — no `execute` method. Instead, the node has
|
|
10
|
+
`requestDefaults` and parameters use `routing` (with `routing.request`,
|
|
11
|
+
`routing.send.preSend`, `routing.output.postReceive`, etc.) to
|
|
12
|
+
describe HTTP calls. See `.agents/nodes-declarative.md`
|
|
13
|
+
- **Programmatic-style** — has an `async execute(this: IExecuteFunctions)`
|
|
14
|
+
method that manually calls APIs (via `this.helpers.httpRequest` /
|
|
15
|
+
`httpRequestWithAuthentication`), loops over items, and builds the
|
|
16
|
+
return array. See `.agents/nodes-programmatic.md`
|
|
17
|
+
|
|
18
|
+
## Node description
|
|
19
|
+
Nodes have `description` which defines:
|
|
20
|
+
- `displayName`, `name`
|
|
21
|
+
- `icon`, `group`, `version`
|
|
22
|
+
- `inputs`, `outputs`
|
|
23
|
+
- `properties`
|
|
24
|
+
- Optional `subtitle`, `usableAsTool`, etc
|
|
25
|
+
and an optional `execute` function for programmatic-style nodes.
|
|
26
|
+
|
|
27
|
+
Example node description (simplified, using WordPress **only as an
|
|
28
|
+
example**):
|
|
29
|
+
```typescript
|
|
30
|
+
description: INodeTypeDescription = {
|
|
31
|
+
displayName: 'Wordpress',
|
|
32
|
+
name: 'wordpress',
|
|
33
|
+
icon: 'file:wordpress.svg',
|
|
34
|
+
group: ['output'],
|
|
35
|
+
version: 1,
|
|
36
|
+
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
|
37
|
+
description: 'Consume Wordpress API',
|
|
38
|
+
defaults: {
|
|
39
|
+
name: 'Wordpress',
|
|
40
|
+
},
|
|
41
|
+
usableAsTool: true,
|
|
42
|
+
inputs: [NodeConnectionTypes.Main],
|
|
43
|
+
outputs: [NodeConnectionTypes.Main],
|
|
44
|
+
credentials: [
|
|
45
|
+
{
|
|
46
|
+
name: 'wordpressApi',
|
|
47
|
+
required: true,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
properties: [
|
|
51
|
+
{
|
|
52
|
+
displayName: 'Resource',
|
|
53
|
+
name: 'resource',
|
|
54
|
+
type: 'options',
|
|
55
|
+
noDataExpression: true,
|
|
56
|
+
options: [
|
|
57
|
+
{ name: 'Post', value: 'post' },
|
|
58
|
+
{ name: 'Page', value: 'page' },
|
|
59
|
+
{ name: 'User', value: 'user' },
|
|
60
|
+
],
|
|
61
|
+
default: 'post',
|
|
62
|
+
},
|
|
63
|
+
// other properties for specific resources and operations
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Description fields
|
|
69
|
+
- `inputs` and `outputs` specify which inputs and outputs a node has.
|
|
70
|
+
- **Most nodes will need only 1 main input and 1 main output, unless
|
|
71
|
+
there is specific reason to have something else** (e.g. a node like
|
|
72
|
+
`If` that has a `true` and `false` outputs).
|
|
73
|
+
- `usableAsTool`
|
|
74
|
+
- Set to `true` to allow n8n to use this node as a tool for the AI
|
|
75
|
+
agent.
|
|
76
|
+
- Set to `false` or omit this if node works heavily with **binary
|
|
77
|
+
data** which tools don't support
|
|
78
|
+
- `properties` define the UI parameters
|
|
79
|
+
- Use the convention: first a **"Resource"** parameter and for each resource an **"Operation"** parameter.
|
|
80
|
+
- You can choose to not follow this convention **ONLY if it's not
|
|
81
|
+
applicable to the node you're developing** (i.e. data transformation
|
|
82
|
+
nodes, etc.)
|
|
83
|
+
|
|
84
|
+
## Resource and operation pattern
|
|
85
|
+
Example "operations":
|
|
86
|
+
```typescript
|
|
87
|
+
export const postOperations: INodeProperties[] = [
|
|
88
|
+
{
|
|
89
|
+
displayName: 'Operation',
|
|
90
|
+
name: 'operation',
|
|
91
|
+
type: 'options',
|
|
92
|
+
noDataExpression: true,
|
|
93
|
+
displayOptions: {
|
|
94
|
+
show: {
|
|
95
|
+
resource: ['post'],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
options: [
|
|
99
|
+
{ name: 'Create', value: 'create', description: 'Create a post', action: 'Create a post', },
|
|
100
|
+
{ name: 'Get', value: 'get', description: 'Get a post', action: 'Get a post', },
|
|
101
|
+
{ name: 'Get Many', value: 'getAll', description: 'Get many posts', action: 'Get many posts', },
|
|
102
|
+
{ name: 'Update', value: 'update', description: 'Update a post', action: 'Update a post', },
|
|
103
|
+
],
|
|
104
|
+
default: 'create',
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
```
|
|
108
|
+
In your implementation:
|
|
109
|
+
- Replace `post` and operation names with the **real resource and operations** for the target API.
|
|
110
|
+
|
|
111
|
+
Example properties for "Create post" operation:
|
|
112
|
+
```typescript
|
|
113
|
+
export const postFields: INodeProperties[] = [
|
|
114
|
+
{
|
|
115
|
+
displayName: 'Title',
|
|
116
|
+
name: 'title',
|
|
117
|
+
type: 'string',
|
|
118
|
+
required: true,
|
|
119
|
+
default: '',
|
|
120
|
+
displayOptions: {
|
|
121
|
+
show: {
|
|
122
|
+
resource: ['post'],
|
|
123
|
+
operation: ['create'],
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
description: 'The title for the post',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
displayName: 'Additional Fields',
|
|
130
|
+
name: 'additionalFields',
|
|
131
|
+
type: 'collection',
|
|
132
|
+
placeholder: 'Add Field',
|
|
133
|
+
default: {},
|
|
134
|
+
displayOptions: {
|
|
135
|
+
show: {
|
|
136
|
+
resource: ['post'],
|
|
137
|
+
operation: ['create'],
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
options: [
|
|
141
|
+
{
|
|
142
|
+
displayName: 'Content',
|
|
143
|
+
name: 'content',
|
|
144
|
+
type: 'string',
|
|
145
|
+
default: '',
|
|
146
|
+
description: 'The content for the post',
|
|
147
|
+
},
|
|
148
|
+
// Add more fields as needed for the real API
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
```
|
|
153
|
+
**Important**:
|
|
154
|
+
- In a real node, replace `post`, `Title`, `Content`, etc. with the
|
|
155
|
+
**real names** from the target API.
|
|
156
|
+
- Do not reuse these exact WordPress-specific field names unless the
|
|
157
|
+
node is actually for WordPress.
|
|
158
|
+
- Remember that these examples are **incomplete** and n8n provides a lot
|
|
159
|
+
of options for defining properties. Refer to their docs, when in doubt
|
|
160
|
+
|
|
161
|
+
## General guidelines
|
|
162
|
+
- `icon` property can either be a string, which starts with `file:` and
|
|
163
|
+
contains a path to a PNG or an SVG. That path **is relative to the current
|
|
164
|
+
file**, you can reference icons in other folders: `file:../icon.svg`. If the
|
|
165
|
+
node has different icons for light and dark mode, provide an object for the
|
|
166
|
+
`icon` property: `{ light: 'file:icon.light.svg', dark: 'file:icon.dark.svg' }`
|
|
167
|
+
- For operations that are supposed to return multiple items, like "Get Many
|
|
168
|
+
Posts", make sure you return those items, instead of single object that has
|
|
169
|
+
them. I.e. if you have an object like `{ data: [{ ... }, { ... }], count: 2 }`,
|
|
170
|
+
then return the items inside `data` array
|
|
171
|
+
- If the API response is complex, you can add a "Simplify Output" toggle. When
|
|
172
|
+
it's `false` - return the raw response. If it's `true` - return a more
|
|
173
|
+
user-friendly response with only the essential data
|
|
174
|
+
- For "Get Many" operations add "Return All" toggle that would return all of
|
|
175
|
+
the items, and a "Limit" parameter to limit the number of items, if "Return
|
|
176
|
+
All" is `false`
|
|
177
|
+
- Don't forget to mark required properties as `required: true`
|
|
178
|
+
- Use camelCase for property names
|