@pagelines/n8n-mcp 0.1.0 → 0.2.1
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/CHANGELOG.md +63 -0
- package/README.md +44 -119
- package/dist/autofix.d.ts +28 -0
- package/dist/autofix.js +222 -0
- package/dist/expressions.d.ts +25 -0
- package/dist/expressions.js +209 -0
- package/dist/index.js +124 -1
- package/dist/tools.js +147 -4
- package/dist/validators.js +67 -0
- package/dist/validators.test.js +83 -0
- package/dist/versions.d.ts +71 -0
- package/dist/versions.js +239 -0
- package/docs/best-practices.md +160 -0
- package/docs/node-config.md +203 -0
- package/package.json +1 -1
- package/plans/ai-guidelines.md +233 -0
- package/plans/architecture.md +177 -0
- package/server.json +10 -2
- package/src/autofix.ts +275 -0
- package/src/expressions.ts +254 -0
- package/src/index.ts +169 -1
- package/src/tools.ts +155 -4
- package/src/validators.test.ts +97 -0
- package/src/validators.ts +77 -0
- package/src/versions.ts +320 -0
package/dist/validators.test.js
CHANGED
|
@@ -95,6 +95,89 @@ describe('validateWorkflow', () => {
|
|
|
95
95
|
severity: 'warning',
|
|
96
96
|
}));
|
|
97
97
|
});
|
|
98
|
+
it('info on code node usage', () => {
|
|
99
|
+
const workflow = createWorkflow({
|
|
100
|
+
nodes: [
|
|
101
|
+
{
|
|
102
|
+
id: '1',
|
|
103
|
+
name: 'my_code',
|
|
104
|
+
type: 'n8n-nodes-base.code',
|
|
105
|
+
typeVersion: 1,
|
|
106
|
+
position: [0, 0],
|
|
107
|
+
parameters: { jsCode: 'return items;' },
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
const result = validateWorkflow(workflow);
|
|
112
|
+
expect(result.warnings).toContainEqual(expect.objectContaining({
|
|
113
|
+
rule: 'code_node_usage',
|
|
114
|
+
severity: 'info',
|
|
115
|
+
}));
|
|
116
|
+
});
|
|
117
|
+
it('warns on AI node without structured output settings', () => {
|
|
118
|
+
const workflow = createWorkflow({
|
|
119
|
+
nodes: [
|
|
120
|
+
{
|
|
121
|
+
id: '1',
|
|
122
|
+
name: 'ai_agent',
|
|
123
|
+
type: '@n8n/n8n-nodes-langchain.agent',
|
|
124
|
+
typeVersion: 1,
|
|
125
|
+
position: [0, 0],
|
|
126
|
+
parameters: {
|
|
127
|
+
outputParser: true,
|
|
128
|
+
schemaType: 'manual',
|
|
129
|
+
// Missing promptType: 'define' and hasOutputParser: true
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
const result = validateWorkflow(workflow);
|
|
135
|
+
expect(result.warnings).toContainEqual(expect.objectContaining({
|
|
136
|
+
rule: 'ai_structured_output',
|
|
137
|
+
severity: 'warning',
|
|
138
|
+
}));
|
|
139
|
+
});
|
|
140
|
+
it('passes AI node with correct structured output settings', () => {
|
|
141
|
+
const workflow = createWorkflow({
|
|
142
|
+
nodes: [
|
|
143
|
+
{
|
|
144
|
+
id: '1',
|
|
145
|
+
name: 'ai_agent',
|
|
146
|
+
type: '@n8n/n8n-nodes-langchain.agent',
|
|
147
|
+
typeVersion: 1,
|
|
148
|
+
position: [0, 0],
|
|
149
|
+
parameters: {
|
|
150
|
+
outputParser: true,
|
|
151
|
+
schemaType: 'manual',
|
|
152
|
+
promptType: 'define',
|
|
153
|
+
hasOutputParser: true,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
});
|
|
158
|
+
const result = validateWorkflow(workflow);
|
|
159
|
+
const aiWarnings = result.warnings.filter((w) => w.rule === 'ai_structured_output');
|
|
160
|
+
expect(aiWarnings).toHaveLength(0);
|
|
161
|
+
});
|
|
162
|
+
it('warns on in-memory storage nodes', () => {
|
|
163
|
+
const workflow = createWorkflow({
|
|
164
|
+
nodes: [
|
|
165
|
+
{
|
|
166
|
+
id: '1',
|
|
167
|
+
name: 'memory_buffer',
|
|
168
|
+
type: '@n8n/n8n-nodes-langchain.memoryBufferWindow',
|
|
169
|
+
typeVersion: 1,
|
|
170
|
+
position: [0, 0],
|
|
171
|
+
parameters: {},
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
});
|
|
175
|
+
const result = validateWorkflow(workflow);
|
|
176
|
+
expect(result.warnings).toContainEqual(expect.objectContaining({
|
|
177
|
+
rule: 'in_memory_storage',
|
|
178
|
+
severity: 'warning',
|
|
179
|
+
}));
|
|
180
|
+
});
|
|
98
181
|
});
|
|
99
182
|
describe('validatePartialUpdate', () => {
|
|
100
183
|
it('errors when node not found', () => {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local version control for n8n workflows
|
|
3
|
+
* Stores workflow snapshots in ~/.n8n-mcp/versions/
|
|
4
|
+
*/
|
|
5
|
+
import type { N8nWorkflow } from './types.js';
|
|
6
|
+
export interface WorkflowVersion {
|
|
7
|
+
id: string;
|
|
8
|
+
workflowId: string;
|
|
9
|
+
workflowName: string;
|
|
10
|
+
timestamp: string;
|
|
11
|
+
reason: string;
|
|
12
|
+
nodeCount: number;
|
|
13
|
+
hash: string;
|
|
14
|
+
}
|
|
15
|
+
export interface VersionConfig {
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
maxVersions: number;
|
|
18
|
+
storageDir: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Initialize version control with custom config
|
|
22
|
+
*/
|
|
23
|
+
export declare function initVersionControl(customConfig?: Partial<VersionConfig>): void;
|
|
24
|
+
/**
|
|
25
|
+
* Save a workflow version
|
|
26
|
+
*/
|
|
27
|
+
export declare function saveVersion(workflow: N8nWorkflow, reason?: string): Promise<WorkflowVersion | null>;
|
|
28
|
+
/**
|
|
29
|
+
* List all versions for a workflow
|
|
30
|
+
*/
|
|
31
|
+
export declare function listVersions(workflowId: string): Promise<WorkflowVersion[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Get a specific version's full workflow data
|
|
34
|
+
*/
|
|
35
|
+
export declare function getVersion(workflowId: string, versionId: string): Promise<{
|
|
36
|
+
meta: WorkflowVersion;
|
|
37
|
+
workflow: N8nWorkflow;
|
|
38
|
+
} | null>;
|
|
39
|
+
/**
|
|
40
|
+
* Get the most recent version
|
|
41
|
+
*/
|
|
42
|
+
export declare function getLatestVersion(workflowId: string): Promise<{
|
|
43
|
+
meta: WorkflowVersion;
|
|
44
|
+
workflow: N8nWorkflow;
|
|
45
|
+
} | null>;
|
|
46
|
+
/**
|
|
47
|
+
* Compare two workflow versions
|
|
48
|
+
*/
|
|
49
|
+
export interface VersionDiff {
|
|
50
|
+
nodesAdded: string[];
|
|
51
|
+
nodesRemoved: string[];
|
|
52
|
+
nodesModified: string[];
|
|
53
|
+
connectionsChanged: boolean;
|
|
54
|
+
settingsChanged: boolean;
|
|
55
|
+
summary: string;
|
|
56
|
+
}
|
|
57
|
+
export declare function diffWorkflows(oldWorkflow: N8nWorkflow, newWorkflow: N8nWorkflow): VersionDiff;
|
|
58
|
+
/**
|
|
59
|
+
* Delete all versions for a workflow
|
|
60
|
+
*/
|
|
61
|
+
export declare function deleteAllVersions(workflowId: string): Promise<number>;
|
|
62
|
+
/**
|
|
63
|
+
* Get version control status/stats
|
|
64
|
+
*/
|
|
65
|
+
export declare function getVersionStats(): Promise<{
|
|
66
|
+
enabled: boolean;
|
|
67
|
+
storageDir: string;
|
|
68
|
+
maxVersions: number;
|
|
69
|
+
workflowCount: number;
|
|
70
|
+
totalVersions: number;
|
|
71
|
+
}>;
|
package/dist/versions.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local version control for n8n workflows
|
|
3
|
+
* Stores workflow snapshots in ~/.n8n-mcp/versions/
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fs } from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import * as os from 'os';
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
enabled: true,
|
|
10
|
+
maxVersions: 20,
|
|
11
|
+
storageDir: path.join(os.homedir(), '.n8n-mcp', 'versions'),
|
|
12
|
+
};
|
|
13
|
+
let config = { ...DEFAULT_CONFIG };
|
|
14
|
+
/**
|
|
15
|
+
* Initialize version control with custom config
|
|
16
|
+
*/
|
|
17
|
+
export function initVersionControl(customConfig = {}) {
|
|
18
|
+
config = { ...DEFAULT_CONFIG, ...customConfig };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get the storage directory for a workflow
|
|
22
|
+
*/
|
|
23
|
+
function getWorkflowDir(workflowId) {
|
|
24
|
+
return path.join(config.storageDir, workflowId);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Generate a simple hash for workflow content
|
|
28
|
+
*/
|
|
29
|
+
function hashWorkflow(workflow) {
|
|
30
|
+
const content = JSON.stringify({
|
|
31
|
+
nodes: workflow.nodes,
|
|
32
|
+
connections: workflow.connections,
|
|
33
|
+
settings: workflow.settings,
|
|
34
|
+
});
|
|
35
|
+
// Simple hash - good enough for comparison
|
|
36
|
+
let hash = 0;
|
|
37
|
+
for (let i = 0; i < content.length; i++) {
|
|
38
|
+
const char = content.charCodeAt(i);
|
|
39
|
+
hash = ((hash << 5) - hash) + char;
|
|
40
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
41
|
+
}
|
|
42
|
+
return Math.abs(hash).toString(16).padStart(8, '0');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Save a workflow version
|
|
46
|
+
*/
|
|
47
|
+
export async function saveVersion(workflow, reason = 'manual') {
|
|
48
|
+
if (!config.enabled)
|
|
49
|
+
return null;
|
|
50
|
+
const workflowDir = getWorkflowDir(workflow.id);
|
|
51
|
+
await fs.mkdir(workflowDir, { recursive: true });
|
|
52
|
+
const timestamp = new Date().toISOString();
|
|
53
|
+
const hash = hashWorkflow(workflow);
|
|
54
|
+
// Check if this exact version already exists (avoid duplicates)
|
|
55
|
+
const existing = await listVersions(workflow.id);
|
|
56
|
+
if (existing.length > 0 && existing[0].hash === hash) {
|
|
57
|
+
return null; // No changes, skip
|
|
58
|
+
}
|
|
59
|
+
const versionId = `${timestamp.replace(/[:.]/g, '-')}_${hash.slice(0, 6)}`;
|
|
60
|
+
const versionFile = path.join(workflowDir, `${versionId}.json`);
|
|
61
|
+
const versionMeta = {
|
|
62
|
+
id: versionId,
|
|
63
|
+
workflowId: workflow.id,
|
|
64
|
+
workflowName: workflow.name,
|
|
65
|
+
timestamp,
|
|
66
|
+
reason,
|
|
67
|
+
nodeCount: workflow.nodes.length,
|
|
68
|
+
hash,
|
|
69
|
+
};
|
|
70
|
+
const versionData = {
|
|
71
|
+
meta: versionMeta,
|
|
72
|
+
workflow,
|
|
73
|
+
};
|
|
74
|
+
await fs.writeFile(versionFile, JSON.stringify(versionData, null, 2));
|
|
75
|
+
// Prune old versions
|
|
76
|
+
await pruneVersions(workflow.id);
|
|
77
|
+
return versionMeta;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* List all versions for a workflow
|
|
81
|
+
*/
|
|
82
|
+
export async function listVersions(workflowId) {
|
|
83
|
+
const workflowDir = getWorkflowDir(workflowId);
|
|
84
|
+
try {
|
|
85
|
+
const files = await fs.readdir(workflowDir);
|
|
86
|
+
const versions = [];
|
|
87
|
+
for (const file of files) {
|
|
88
|
+
if (!file.endsWith('.json'))
|
|
89
|
+
continue;
|
|
90
|
+
const filePath = path.join(workflowDir, file);
|
|
91
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
92
|
+
const data = JSON.parse(content);
|
|
93
|
+
if (data.meta) {
|
|
94
|
+
versions.push(data.meta);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Sort by timestamp descending (newest first)
|
|
98
|
+
return versions.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error.code === 'ENOENT') {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get a specific version's full workflow data
|
|
109
|
+
*/
|
|
110
|
+
export async function getVersion(workflowId, versionId) {
|
|
111
|
+
const workflowDir = getWorkflowDir(workflowId);
|
|
112
|
+
const versionFile = path.join(workflowDir, `${versionId}.json`);
|
|
113
|
+
try {
|
|
114
|
+
const content = await fs.readFile(versionFile, 'utf-8');
|
|
115
|
+
return JSON.parse(content);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error.code === 'ENOENT') {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get the most recent version
|
|
126
|
+
*/
|
|
127
|
+
export async function getLatestVersion(workflowId) {
|
|
128
|
+
const versions = await listVersions(workflowId);
|
|
129
|
+
if (versions.length === 0)
|
|
130
|
+
return null;
|
|
131
|
+
return getVersion(workflowId, versions[0].id);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Prune old versions beyond maxVersions
|
|
135
|
+
*/
|
|
136
|
+
async function pruneVersions(workflowId) {
|
|
137
|
+
const versions = await listVersions(workflowId);
|
|
138
|
+
if (versions.length <= config.maxVersions) {
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
const toDelete = versions.slice(config.maxVersions);
|
|
142
|
+
const workflowDir = getWorkflowDir(workflowId);
|
|
143
|
+
for (const version of toDelete) {
|
|
144
|
+
const versionFile = path.join(workflowDir, `${version.id}.json`);
|
|
145
|
+
await fs.unlink(versionFile);
|
|
146
|
+
}
|
|
147
|
+
return toDelete.length;
|
|
148
|
+
}
|
|
149
|
+
export function diffWorkflows(oldWorkflow, newWorkflow) {
|
|
150
|
+
const oldNodes = new Map(oldWorkflow.nodes.map((n) => [n.name, n]));
|
|
151
|
+
const newNodes = new Map(newWorkflow.nodes.map((n) => [n.name, n]));
|
|
152
|
+
const nodesAdded = [];
|
|
153
|
+
const nodesRemoved = [];
|
|
154
|
+
const nodesModified = [];
|
|
155
|
+
// Find added and modified nodes
|
|
156
|
+
for (const [name, node] of newNodes) {
|
|
157
|
+
if (!oldNodes.has(name)) {
|
|
158
|
+
nodesAdded.push(name);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
const oldNode = oldNodes.get(name);
|
|
162
|
+
if (JSON.stringify(oldNode.parameters) !== JSON.stringify(node.parameters)) {
|
|
163
|
+
nodesModified.push(name);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Find removed nodes
|
|
168
|
+
for (const name of oldNodes.keys()) {
|
|
169
|
+
if (!newNodes.has(name)) {
|
|
170
|
+
nodesRemoved.push(name);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const connectionsChanged = JSON.stringify(oldWorkflow.connections) !== JSON.stringify(newWorkflow.connections);
|
|
174
|
+
const settingsChanged = JSON.stringify(oldWorkflow.settings) !== JSON.stringify(newWorkflow.settings);
|
|
175
|
+
// Generate summary
|
|
176
|
+
const parts = [];
|
|
177
|
+
if (nodesAdded.length)
|
|
178
|
+
parts.push(`+${nodesAdded.length} nodes`);
|
|
179
|
+
if (nodesRemoved.length)
|
|
180
|
+
parts.push(`-${nodesRemoved.length} nodes`);
|
|
181
|
+
if (nodesModified.length)
|
|
182
|
+
parts.push(`~${nodesModified.length} modified`);
|
|
183
|
+
if (connectionsChanged)
|
|
184
|
+
parts.push('connections changed');
|
|
185
|
+
if (settingsChanged)
|
|
186
|
+
parts.push('settings changed');
|
|
187
|
+
return {
|
|
188
|
+
nodesAdded,
|
|
189
|
+
nodesRemoved,
|
|
190
|
+
nodesModified,
|
|
191
|
+
connectionsChanged,
|
|
192
|
+
settingsChanged,
|
|
193
|
+
summary: parts.length ? parts.join(', ') : 'no changes',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Delete all versions for a workflow
|
|
198
|
+
*/
|
|
199
|
+
export async function deleteAllVersions(workflowId) {
|
|
200
|
+
const versions = await listVersions(workflowId);
|
|
201
|
+
const workflowDir = getWorkflowDir(workflowId);
|
|
202
|
+
for (const version of versions) {
|
|
203
|
+
const versionFile = path.join(workflowDir, `${version.id}.json`);
|
|
204
|
+
await fs.unlink(versionFile);
|
|
205
|
+
}
|
|
206
|
+
// Remove the directory if empty
|
|
207
|
+
try {
|
|
208
|
+
await fs.rmdir(workflowDir);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// Ignore if not empty
|
|
212
|
+
}
|
|
213
|
+
return versions.length;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Get version control status/stats
|
|
217
|
+
*/
|
|
218
|
+
export async function getVersionStats() {
|
|
219
|
+
let workflowCount = 0;
|
|
220
|
+
let totalVersions = 0;
|
|
221
|
+
try {
|
|
222
|
+
const workflows = await fs.readdir(config.storageDir);
|
|
223
|
+
workflowCount = workflows.length;
|
|
224
|
+
for (const workflowId of workflows) {
|
|
225
|
+
const versions = await listVersions(workflowId);
|
|
226
|
+
totalVersions += versions.length;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// Storage dir doesn't exist yet
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
enabled: config.enabled,
|
|
234
|
+
storageDir: config.storageDir,
|
|
235
|
+
maxVersions: config.maxVersions,
|
|
236
|
+
workflowCount,
|
|
237
|
+
totalVersions,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# n8n Best Practices
|
|
2
|
+
|
|
3
|
+
## Quick Reference
|
|
4
|
+
|
|
5
|
+
```javascript
|
|
6
|
+
// Explicit reference (always use this)
|
|
7
|
+
{{ $('node_name').item.json.field }}
|
|
8
|
+
|
|
9
|
+
// Environment variable
|
|
10
|
+
{{ $env.API_KEY }}
|
|
11
|
+
|
|
12
|
+
// Config node reference
|
|
13
|
+
{{ $('config').item.json.setting }}
|
|
14
|
+
|
|
15
|
+
// Fallback
|
|
16
|
+
{{ $('source').item.json.text || 'default' }}
|
|
17
|
+
|
|
18
|
+
// Date
|
|
19
|
+
{{ $now.format('yyyy-MM-dd') }}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## The Rules
|
|
23
|
+
|
|
24
|
+
### 1. snake_case
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
Good: fetch_articles, check_approved, generate_content
|
|
28
|
+
Bad: FetchArticles, Check Approved, generate-content
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Why: Consistency, readability, auto-fixable.
|
|
32
|
+
|
|
33
|
+
### 2. Explicit References
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
// Bad - breaks when flow changes
|
|
37
|
+
{{ $json.field }}
|
|
38
|
+
|
|
39
|
+
// Good - traceable, stable
|
|
40
|
+
{{ $('node_name').item.json.field }}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Why: `$json` references "previous node" implicitly. Reorder nodes, it breaks.
|
|
44
|
+
|
|
45
|
+
### 3. Config Node
|
|
46
|
+
|
|
47
|
+
Single source for workflow settings:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
[trigger] → [config] → [rest of workflow]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Config node (JSON mode):
|
|
54
|
+
```javascript
|
|
55
|
+
={
|
|
56
|
+
"channel_id": "{{ $json.body.channelId || '123456' }}",
|
|
57
|
+
"max_items": 10,
|
|
58
|
+
"ai_model": "gpt-4.1-mini"
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Reference everywhere: `{{ $('config').item.json.channel_id }}`
|
|
63
|
+
|
|
64
|
+
Why: Change once, not in 5 nodes.
|
|
65
|
+
|
|
66
|
+
### 4. Secrets in Environment
|
|
67
|
+
|
|
68
|
+
```javascript
|
|
69
|
+
// Bad
|
|
70
|
+
{ "apiKey": "sk_live_abc123" }
|
|
71
|
+
|
|
72
|
+
// Good
|
|
73
|
+
{{ $env.API_KEY }}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Parameter Preservation
|
|
77
|
+
|
|
78
|
+
**Critical:** Partial updates REPLACE the entire `parameters` object.
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
// Bad - loses operation and labelIds
|
|
82
|
+
{
|
|
83
|
+
"type": "updateNode",
|
|
84
|
+
"nodeName": "archive_email",
|
|
85
|
+
"properties": {
|
|
86
|
+
"parameters": {
|
|
87
|
+
"messageId": "={{ $json.message_id }}"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Good - include ALL parameters
|
|
93
|
+
{
|
|
94
|
+
"type": "updateNode",
|
|
95
|
+
"nodeName": "archive_email",
|
|
96
|
+
"properties": {
|
|
97
|
+
"parameters": {
|
|
98
|
+
"operation": "addLabels",
|
|
99
|
+
"messageId": "={{ $json.message_id }}",
|
|
100
|
+
"labelIds": ["Label_123"]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Before updating: read current state with `workflow_get`.
|
|
107
|
+
|
|
108
|
+
## AI Nodes
|
|
109
|
+
|
|
110
|
+
### Structured Output
|
|
111
|
+
|
|
112
|
+
Always set for predictable JSON:
|
|
113
|
+
|
|
114
|
+
| Setting | Value |
|
|
115
|
+
|---------|-------|
|
|
116
|
+
| `promptType` | `"define"` |
|
|
117
|
+
| `hasOutputParser` | `true` |
|
|
118
|
+
| `schemaType` | `"manual"` (for nullable fields) |
|
|
119
|
+
|
|
120
|
+
### Memory
|
|
121
|
+
|
|
122
|
+
| Don't Use | Use Instead |
|
|
123
|
+
|-----------|-------------|
|
|
124
|
+
| Windowed Buffer Memory | Postgres Chat Memory |
|
|
125
|
+
| In-Memory Vector Store | Postgres pgvector |
|
|
126
|
+
|
|
127
|
+
In-memory dies on restart, doesn't scale.
|
|
128
|
+
|
|
129
|
+
## Code Nodes: Last Resort
|
|
130
|
+
|
|
131
|
+
| Need | Use Instead |
|
|
132
|
+
|------|-------------|
|
|
133
|
+
| Transform fields | Set node with expressions |
|
|
134
|
+
| Filter items | Filter node or Switch |
|
|
135
|
+
| Merge data | Merge node |
|
|
136
|
+
| Loop | n8n processes arrays natively |
|
|
137
|
+
| Date formatting | `{{ $now.format('yyyy-MM-dd') }}` |
|
|
138
|
+
|
|
139
|
+
When code IS necessary:
|
|
140
|
+
- Re-establishing `pairedItem` after chain breaks
|
|
141
|
+
- Complex conditional logic
|
|
142
|
+
- API parsing expressions can't handle
|
|
143
|
+
|
|
144
|
+
## Pre-Edit Checklist
|
|
145
|
+
|
|
146
|
+
| Step | Why |
|
|
147
|
+
|------|-----|
|
|
148
|
+
| 1. Get explicit user approval | Don't surprise |
|
|
149
|
+
| 2. List versions | Know rollback point |
|
|
150
|
+
| 3. Read full workflow | Understand current state |
|
|
151
|
+
| 4. Make targeted change | Minimal surface area |
|
|
152
|
+
| 5. Validate after | Catch issues immediately |
|
|
153
|
+
|
|
154
|
+
## Node-Specific Settings
|
|
155
|
+
|
|
156
|
+
See [Node Config](node-config.md) for:
|
|
157
|
+
- Resource locator (`__rl`) format with `cachedResultName`
|
|
158
|
+
- Google Sheets, Gmail, Discord required fields
|
|
159
|
+
- Set node JSON mode vs manual mapping
|
|
160
|
+
- Error handling options
|