@pagelines/n8n-mcp 0.2.0 → 0.3.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/CHANGELOG.md +37 -0
- package/README.md +70 -60
- package/dist/index.js +122 -18
- package/dist/n8n-client.d.ts +3 -2
- package/dist/n8n-client.js +9 -1
- package/dist/n8n-client.test.js +111 -0
- package/dist/response-format.d.ts +84 -0
- package/dist/response-format.js +183 -0
- package/dist/response-format.test.d.ts +1 -0
- package/dist/response-format.test.js +291 -0
- package/dist/tools.js +67 -3
- package/dist/types.d.ts +27 -0
- package/dist/validators.d.ts +9 -1
- package/dist/validators.js +87 -2
- package/dist/validators.test.js +83 -4
- package/docs/best-practices.md +80 -156
- package/docs/node-config.md +205 -0
- package/logo.png +0 -0
- package/package.json +1 -1
- package/plans/ai-guidelines.md +233 -0
- package/plans/architecture.md +220 -0
- package/src/index.ts +159 -20
- package/src/n8n-client.test.ts +135 -0
- package/src/n8n-client.ts +13 -2
- package/src/response-format.test.ts +355 -0
- package/src/response-format.ts +278 -0
- package/src/tools.ts +68 -3
- package/src/types.ts +33 -0
- package/src/validators.test.ts +101 -4
- package/src/validators.ts +112 -3
package/dist/validators.js
CHANGED
|
@@ -106,8 +106,8 @@ function checkForHardcodedSecrets(node, warnings) {
|
|
|
106
106
|
warnings.push({
|
|
107
107
|
node: node.name,
|
|
108
108
|
rule: 'no_hardcoded_secrets',
|
|
109
|
-
message: `Node "${node.name}" may contain hardcoded secrets -
|
|
110
|
-
severity: '
|
|
109
|
+
message: `Node "${node.name}" may contain hardcoded secrets - consider using $env.VAR_NAME`,
|
|
110
|
+
severity: 'info',
|
|
111
111
|
});
|
|
112
112
|
break;
|
|
113
113
|
}
|
|
@@ -236,3 +236,88 @@ export function validatePartialUpdate(currentWorkflow, nodeName, newParameters)
|
|
|
236
236
|
}
|
|
237
237
|
return warnings;
|
|
238
238
|
}
|
|
239
|
+
// ─────────────────────────────────────────────────────────────
|
|
240
|
+
// Node Type Validation
|
|
241
|
+
// ─────────────────────────────────────────────────────────────
|
|
242
|
+
/**
|
|
243
|
+
* Validate that all node types in an array exist in the available types
|
|
244
|
+
* Returns errors for any invalid node types with suggestions
|
|
245
|
+
*/
|
|
246
|
+
export function validateNodeTypes(nodes, availableTypes) {
|
|
247
|
+
const errors = [];
|
|
248
|
+
for (const node of nodes) {
|
|
249
|
+
if (!availableTypes.has(node.type)) {
|
|
250
|
+
errors.push({
|
|
251
|
+
nodeType: node.type,
|
|
252
|
+
nodeName: node.name,
|
|
253
|
+
message: `Invalid node type "${node.type}" for node "${node.name}"`,
|
|
254
|
+
suggestions: findSimilarTypes(node.type, availableTypes),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return errors;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Find similar node types for suggestions (fuzzy matching)
|
|
262
|
+
* Returns up to 3 suggestions
|
|
263
|
+
*/
|
|
264
|
+
function findSimilarTypes(invalidType, availableTypes) {
|
|
265
|
+
const suggestions = [];
|
|
266
|
+
const searchTerm = invalidType.toLowerCase();
|
|
267
|
+
// Extract the last part after the dot (e.g., "webhook" from "n8n-nodes-base.webhook")
|
|
268
|
+
const typeParts = searchTerm.split('.');
|
|
269
|
+
const shortName = typeParts[typeParts.length - 1];
|
|
270
|
+
for (const validType of availableTypes) {
|
|
271
|
+
const validLower = validType.toLowerCase();
|
|
272
|
+
const validShortName = validLower.split('.').pop() || '';
|
|
273
|
+
// Check for partial matches (substring)
|
|
274
|
+
if (validLower.includes(shortName) ||
|
|
275
|
+
shortName.includes(validShortName) ||
|
|
276
|
+
validShortName.includes(shortName)) {
|
|
277
|
+
suggestions.push(validType);
|
|
278
|
+
if (suggestions.length >= 3)
|
|
279
|
+
break;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
// Check for typos using Levenshtein distance
|
|
283
|
+
const distance = levenshteinDistance(shortName, validShortName);
|
|
284
|
+
const maxLen = Math.max(shortName.length, validShortName.length);
|
|
285
|
+
// Allow up to 2 character differences for short names, or 20% of length for longer ones
|
|
286
|
+
const threshold = Math.max(2, Math.floor(maxLen * 0.2));
|
|
287
|
+
if (distance <= threshold) {
|
|
288
|
+
suggestions.push(validType);
|
|
289
|
+
if (suggestions.length >= 3)
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return suggestions;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Calculate Levenshtein distance between two strings
|
|
297
|
+
*/
|
|
298
|
+
function levenshteinDistance(a, b) {
|
|
299
|
+
if (a.length === 0)
|
|
300
|
+
return b.length;
|
|
301
|
+
if (b.length === 0)
|
|
302
|
+
return a.length;
|
|
303
|
+
const matrix = [];
|
|
304
|
+
// Initialize first column
|
|
305
|
+
for (let i = 0; i <= a.length; i++) {
|
|
306
|
+
matrix[i] = [i];
|
|
307
|
+
}
|
|
308
|
+
// Initialize first row
|
|
309
|
+
for (let j = 0; j <= b.length; j++) {
|
|
310
|
+
matrix[0][j] = j;
|
|
311
|
+
}
|
|
312
|
+
// Fill in the rest of the matrix
|
|
313
|
+
for (let i = 1; i <= a.length; i++) {
|
|
314
|
+
for (let j = 1; j <= b.length; j++) {
|
|
315
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
316
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, // deletion
|
|
317
|
+
matrix[i][j - 1] + 1, // insertion
|
|
318
|
+
matrix[i - 1][j - 1] + cost // substitution
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return matrix[a.length][b.length];
|
|
323
|
+
}
|
package/dist/validators.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { validateWorkflow, validatePartialUpdate } from './validators.js';
|
|
2
|
+
import { validateWorkflow, validatePartialUpdate, validateNodeTypes } from './validators.js';
|
|
3
3
|
const createWorkflow = (overrides = {}) => ({
|
|
4
4
|
id: '1',
|
|
5
5
|
name: 'test_workflow',
|
|
@@ -55,7 +55,7 @@ describe('validateWorkflow', () => {
|
|
|
55
55
|
severity: 'warning',
|
|
56
56
|
}));
|
|
57
57
|
});
|
|
58
|
-
it('
|
|
58
|
+
it('warns on hardcoded secrets', () => {
|
|
59
59
|
const workflow = createWorkflow({
|
|
60
60
|
nodes: [
|
|
61
61
|
{
|
|
@@ -69,10 +69,9 @@ describe('validateWorkflow', () => {
|
|
|
69
69
|
],
|
|
70
70
|
});
|
|
71
71
|
const result = validateWorkflow(workflow);
|
|
72
|
-
expect(result.valid).toBe(false);
|
|
73
72
|
expect(result.warnings).toContainEqual(expect.objectContaining({
|
|
74
73
|
rule: 'no_hardcoded_secrets',
|
|
75
|
-
severity: '
|
|
74
|
+
severity: 'info',
|
|
76
75
|
}));
|
|
77
76
|
});
|
|
78
77
|
it('warns on orphan nodes', () => {
|
|
@@ -229,3 +228,83 @@ describe('validatePartialUpdate', () => {
|
|
|
229
228
|
expect(warnings).toHaveLength(0);
|
|
230
229
|
});
|
|
231
230
|
});
|
|
231
|
+
describe('validateNodeTypes', () => {
|
|
232
|
+
const availableTypes = new Set([
|
|
233
|
+
'n8n-nodes-base.webhook',
|
|
234
|
+
'n8n-nodes-base.set',
|
|
235
|
+
'n8n-nodes-base.code',
|
|
236
|
+
'n8n-nodes-base.httpRequest',
|
|
237
|
+
'@n8n/n8n-nodes-langchain.agent',
|
|
238
|
+
'@n8n/n8n-nodes-langchain.chatTrigger',
|
|
239
|
+
]);
|
|
240
|
+
it('passes when all node types are valid', () => {
|
|
241
|
+
const nodes = [
|
|
242
|
+
{ name: 'webhook_trigger', type: 'n8n-nodes-base.webhook' },
|
|
243
|
+
{ name: 'set_data', type: 'n8n-nodes-base.set' },
|
|
244
|
+
{ name: 'ai_agent', type: '@n8n/n8n-nodes-langchain.agent' },
|
|
245
|
+
];
|
|
246
|
+
const errors = validateNodeTypes(nodes, availableTypes);
|
|
247
|
+
expect(errors).toHaveLength(0);
|
|
248
|
+
});
|
|
249
|
+
it('returns error for invalid node type', () => {
|
|
250
|
+
const nodes = [
|
|
251
|
+
{ name: 'my_node', type: 'n8n-nodes-base.nonexistent' },
|
|
252
|
+
];
|
|
253
|
+
const errors = validateNodeTypes(nodes, availableTypes);
|
|
254
|
+
expect(errors).toHaveLength(1);
|
|
255
|
+
expect(errors[0]).toEqual(expect.objectContaining({
|
|
256
|
+
nodeType: 'n8n-nodes-base.nonexistent',
|
|
257
|
+
nodeName: 'my_node',
|
|
258
|
+
}));
|
|
259
|
+
});
|
|
260
|
+
it('returns errors for multiple invalid node types', () => {
|
|
261
|
+
const nodes = [
|
|
262
|
+
{ name: 'valid_node', type: 'n8n-nodes-base.webhook' },
|
|
263
|
+
{ name: 'invalid_one', type: 'n8n-nodes-base.fake' },
|
|
264
|
+
{ name: 'invalid_two', type: 'n8n-nodes-base.bogus' },
|
|
265
|
+
];
|
|
266
|
+
const errors = validateNodeTypes(nodes, availableTypes);
|
|
267
|
+
expect(errors).toHaveLength(2);
|
|
268
|
+
expect(errors.map((e) => e.nodeName)).toEqual(['invalid_one', 'invalid_two']);
|
|
269
|
+
});
|
|
270
|
+
it('provides suggestions for typos', () => {
|
|
271
|
+
const nodes = [
|
|
272
|
+
{ name: 'trigger', type: 'n8n-nodes-base.webhok' }, // typo: webhok
|
|
273
|
+
];
|
|
274
|
+
const errors = validateNodeTypes(nodes, availableTypes);
|
|
275
|
+
expect(errors).toHaveLength(1);
|
|
276
|
+
expect(errors[0].suggestions).toContain('n8n-nodes-base.webhook');
|
|
277
|
+
});
|
|
278
|
+
it('provides suggestions for partial matches', () => {
|
|
279
|
+
const nodes = [
|
|
280
|
+
{ name: 'code_node', type: 'n8n-nodes-base.cod' }, // partial: cod
|
|
281
|
+
];
|
|
282
|
+
const errors = validateNodeTypes(nodes, availableTypes);
|
|
283
|
+
expect(errors).toHaveLength(1);
|
|
284
|
+
expect(errors[0].suggestions).toContain('n8n-nodes-base.code');
|
|
285
|
+
});
|
|
286
|
+
it('returns empty suggestions when no matches found', () => {
|
|
287
|
+
const nodes = [
|
|
288
|
+
{ name: 'xyz_node', type: 'n8n-nodes-base.xyz123completely_random' },
|
|
289
|
+
];
|
|
290
|
+
const errors = validateNodeTypes(nodes, availableTypes);
|
|
291
|
+
expect(errors).toHaveLength(1);
|
|
292
|
+
expect(errors[0].suggestions).toHaveLength(0);
|
|
293
|
+
});
|
|
294
|
+
it('limits suggestions to 3', () => {
|
|
295
|
+
// Create a set with many similar types
|
|
296
|
+
const manyTypes = new Set([
|
|
297
|
+
'n8n-nodes-base.httpRequest',
|
|
298
|
+
'n8n-nodes-base.httpRequestTool',
|
|
299
|
+
'n8n-nodes-base.httpRequestV1',
|
|
300
|
+
'n8n-nodes-base.httpRequestV2',
|
|
301
|
+
'n8n-nodes-base.httpRequestV3',
|
|
302
|
+
]);
|
|
303
|
+
const nodes = [
|
|
304
|
+
{ name: 'http', type: 'n8n-nodes-base.http' }, // should match multiple
|
|
305
|
+
];
|
|
306
|
+
const errors = validateNodeTypes(nodes, manyTypes);
|
|
307
|
+
expect(errors).toHaveLength(1);
|
|
308
|
+
expect(errors[0].suggestions.length).toBeLessThanOrEqual(3);
|
|
309
|
+
});
|
|
310
|
+
});
|
package/docs/best-practices.md
CHANGED
|
@@ -1,107 +1,88 @@
|
|
|
1
1
|
# n8n Best Practices
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> **These rules are automatically enforced.** The MCP validates, auto-fixes, and formats on every `workflow_create` and `workflow_update`. You'll only see warnings for issues that can't be auto-fixed.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
**What is most stable and easiest to maintain?**
|
|
5
|
+
## Quick Reference
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
| YAGNI | Build only what's needed now |
|
|
13
|
-
| Explicit references | `$('node_name')` not `$json` - traceable, stable |
|
|
14
|
-
| snake_case | `node_name` not `NodeName` - consistent, readable |
|
|
7
|
+
```javascript
|
|
8
|
+
// Explicit reference (always use this)
|
|
9
|
+
{{ $('node_name').item.json.field }}
|
|
15
10
|
|
|
16
|
-
|
|
11
|
+
// Environment variable
|
|
12
|
+
{{ $env.API_KEY }}
|
|
17
13
|
|
|
18
|
-
|
|
14
|
+
// Config node reference
|
|
15
|
+
{{ $('config').item.json.setting }}
|
|
19
16
|
|
|
20
|
-
|
|
17
|
+
// Fallback
|
|
18
|
+
{{ $('source').item.json.text || 'default' }}
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
Nodes: trigger_webhook, fetch_articles, check_approved
|
|
20
|
+
// Date
|
|
21
|
+
{{ $now.format('yyyy-MM-dd') }}
|
|
25
22
|
```
|
|
26
23
|
|
|
27
|
-
|
|
24
|
+
## The Rules
|
|
28
25
|
|
|
29
|
-
|
|
26
|
+
### 1. snake_case (auto-fixed)
|
|
30
27
|
|
|
31
|
-
|
|
28
|
+
```
|
|
29
|
+
Good: fetch_articles, check_approved, generate_content
|
|
30
|
+
Bad: FetchArticles, Check Approved, generate-content
|
|
31
|
+
```
|
|
32
32
|
|
|
33
|
-
**
|
|
33
|
+
Why: Consistency, readability. **Auto-fixed:** renamed automatically with all references updated.
|
|
34
|
+
|
|
35
|
+
### 2. Explicit References (auto-fixed)
|
|
34
36
|
|
|
35
37
|
```javascript
|
|
36
38
|
// Bad - breaks when flow changes
|
|
37
39
|
{{ $json.field }}
|
|
38
40
|
|
|
39
|
-
// Good - traceable
|
|
41
|
+
// Good - traceable, stable
|
|
40
42
|
{{ $('node_name').item.json.field }}
|
|
41
|
-
|
|
42
|
-
// Parallel branch (lookup nodes)
|
|
43
|
-
{{ $('lookup_node').all().length > 0 }}
|
|
44
|
-
|
|
45
|
-
// Environment variable
|
|
46
|
-
{{ $env.API_KEY }}
|
|
47
|
-
|
|
48
|
-
// Fallback pattern
|
|
49
|
-
{{ $('source').item.json.text || $('source').item.json.media_url }}
|
|
50
43
|
```
|
|
51
44
|
|
|
52
|
-
|
|
45
|
+
Why: `$json` references "previous node" implicitly. Reorder nodes, it breaks. **Auto-fixed:** converted to explicit `$('prev_node')` references.
|
|
53
46
|
|
|
54
|
-
|
|
47
|
+
### 3. Config Node
|
|
55
48
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
```javascript
|
|
59
|
-
// Bad
|
|
60
|
-
{ "apiKey": "sk_live_abc123" }
|
|
61
|
-
|
|
62
|
-
// Good
|
|
63
|
-
{{ $env.API_KEY }}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
**For workflow-specific settings, use a config node:**
|
|
49
|
+
Single source for workflow settings:
|
|
67
50
|
|
|
68
51
|
```
|
|
69
52
|
[trigger] → [config] → [rest of workflow]
|
|
70
53
|
```
|
|
71
54
|
|
|
72
|
-
Config node
|
|
55
|
+
Config node (JSON mode):
|
|
73
56
|
```javascript
|
|
74
57
|
={
|
|
75
|
-
"channel_id": "{{ $json.body.channelId || '
|
|
58
|
+
"channel_id": "{{ $json.body.channelId || '123456' }}",
|
|
76
59
|
"max_items": 10,
|
|
77
60
|
"ai_model": "gpt-4.1-mini"
|
|
78
61
|
}
|
|
79
62
|
```
|
|
80
63
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
---
|
|
64
|
+
Reference everywhere: `{{ $('config').item.json.channel_id }}`
|
|
84
65
|
|
|
85
|
-
|
|
66
|
+
Why: Change once, not in 5 nodes.
|
|
86
67
|
|
|
87
|
-
###
|
|
68
|
+
### 4. Secrets in Environment (recommended)
|
|
88
69
|
|
|
89
|
-
|
|
70
|
+
```javascript
|
|
71
|
+
// Hardcoded (works, but less portable)
|
|
72
|
+
{ "apiKey": "sk_live_abc123" }
|
|
90
73
|
|
|
91
|
-
|
|
74
|
+
// Environment variable (recommended)
|
|
75
|
+
{{ $env.API_KEY }}
|
|
76
|
+
```
|
|
92
77
|
|
|
93
|
-
|
|
94
|
-
2. **List versions** - Know your rollback point
|
|
95
|
-
3. **Read full state** - Understand current config
|
|
96
|
-
4. **Make targeted change** - Use patch operations only
|
|
97
|
-
5. **Verify** - Confirm expected state
|
|
78
|
+
Why: Env vars make workflows portable across environments and avoid committing secrets.
|
|
98
79
|
|
|
99
|
-
|
|
80
|
+
## Parameter Preservation
|
|
100
81
|
|
|
101
|
-
**
|
|
82
|
+
**Critical:** Partial updates REPLACE the entire `parameters` object.
|
|
102
83
|
|
|
103
84
|
```javascript
|
|
104
|
-
// Bad
|
|
85
|
+
// Bad - loses operation and labelIds
|
|
105
86
|
{
|
|
106
87
|
"type": "updateNode",
|
|
107
88
|
"nodeName": "archive_email",
|
|
@@ -112,7 +93,7 @@ Then reference: `{{ $('config').item.json.channel_id }}`
|
|
|
112
93
|
}
|
|
113
94
|
}
|
|
114
95
|
|
|
115
|
-
// Good
|
|
96
|
+
// Good - include ALL parameters
|
|
116
97
|
{
|
|
117
98
|
"type": "updateNode",
|
|
118
99
|
"nodeName": "archive_email",
|
|
@@ -120,122 +101,65 @@ Then reference: `{{ $('config').item.json.channel_id }}`
|
|
|
120
101
|
"parameters": {
|
|
121
102
|
"operation": "addLabels",
|
|
122
103
|
"messageId": "={{ $json.message_id }}",
|
|
123
|
-
"labelIds": ["Label_123"
|
|
104
|
+
"labelIds": ["Label_123"]
|
|
124
105
|
}
|
|
125
106
|
}
|
|
126
107
|
}
|
|
127
108
|
```
|
|
128
109
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
## Code Nodes
|
|
132
|
-
|
|
133
|
-
**Code nodes are a last resort.** Exhaust built-in options first:
|
|
110
|
+
Before updating: read current state with `workflow_get`.
|
|
134
111
|
|
|
135
|
-
|
|
136
|
-
|------|-------------|
|
|
137
|
-
| Transform fields | Set node with expressions |
|
|
138
|
-
| Filter items | Filter node or If/Switch |
|
|
139
|
-
| Merge data | Merge node |
|
|
140
|
-
| Loop processing | n8n processes arrays natively |
|
|
141
|
-
| Date formatting | `{{ $now.format('yyyy-MM-dd') }}` |
|
|
112
|
+
## AI Nodes
|
|
142
113
|
|
|
143
|
-
|
|
144
|
-
- Re-establishing `pairedItem` after chain breaks
|
|
145
|
-
- Complex conditional logic
|
|
146
|
-
- API response parsing expressions can't handle
|
|
114
|
+
### Structured Output (auto-fixed)
|
|
147
115
|
|
|
148
|
-
**
|
|
149
|
-
- Single responsibility (one clear purpose)
|
|
150
|
-
- Name it for what it does: `merge_context`, `parse_response`
|
|
151
|
-
- No side effects - pure data transformation
|
|
116
|
+
Always set for predictable JSON. **Auto-fixed:** `promptType: "define"` and `hasOutputParser: true` added automatically.
|
|
152
117
|
|
|
153
|
-
|
|
118
|
+
| Setting | Value |
|
|
119
|
+
|---------|-------|
|
|
120
|
+
| `promptType` | `"define"` |
|
|
121
|
+
| `hasOutputParser` | `true` |
|
|
122
|
+
| `schemaType` | `"manual"` (for nullable fields) |
|
|
154
123
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
### Structured Output
|
|
158
|
-
|
|
159
|
-
**Always enable "Require Specific Output Format"** for reliable JSON:
|
|
160
|
-
|
|
161
|
-
```javascript
|
|
162
|
-
{
|
|
163
|
-
"promptType": "define",
|
|
164
|
-
"hasOutputParser": true,
|
|
165
|
-
"schemaType": "manual" // Required for nullable fields
|
|
166
|
-
}
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
Without these, AI outputs are unpredictable.
|
|
170
|
-
|
|
171
|
-
### Memory Storage
|
|
172
|
-
|
|
173
|
-
**Never use in-memory storage in production:**
|
|
124
|
+
### Memory
|
|
174
125
|
|
|
175
126
|
| Don't Use | Use Instead |
|
|
176
127
|
|-----------|-------------|
|
|
177
128
|
| Windowed Buffer Memory | Postgres Chat Memory |
|
|
178
129
|
| In-Memory Vector Store | Postgres pgvector |
|
|
179
130
|
|
|
180
|
-
In-memory dies
|
|
181
|
-
|
|
182
|
-
---
|
|
183
|
-
|
|
184
|
-
## Architecture Patterns
|
|
185
|
-
|
|
186
|
-
### Single Responsibility
|
|
187
|
-
|
|
188
|
-
Don't build monolith workflows:
|
|
189
|
-
|
|
190
|
-
```
|
|
191
|
-
Bad: One workflow doing signup → email → CRM → calendar → reports
|
|
192
|
-
|
|
193
|
-
Good: Five focused workflows that communicate via webhooks
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### Switch > If Node
|
|
197
|
-
|
|
198
|
-
Always use Switch instead of If:
|
|
199
|
-
- Named outputs (not just true/false)
|
|
200
|
-
- Unlimited conditional branches
|
|
201
|
-
- Send to all matching option
|
|
131
|
+
In-memory dies on restart, doesn't scale.
|
|
202
132
|
|
|
203
|
-
|
|
133
|
+
## Code Nodes: Last Resort
|
|
204
134
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
|
208
|
-
|
|
209
|
-
|
|
|
210
|
-
|
|
|
211
|
-
|
|
|
212
|
-
| `no_hardcoded_secrets` | error | Never hardcode secrets |
|
|
213
|
-
| `orphan_node` | warning | Node has no connections |
|
|
214
|
-
| `parameter_preservation` | error | Update would remove parameters |
|
|
215
|
-
| `code_node_usage` | info | Code node detected |
|
|
216
|
-
| `ai_structured_output` | warning | AI node missing structured output |
|
|
217
|
-
| `in_memory_storage` | warning | Using non-persistent storage |
|
|
218
|
-
|
|
219
|
-
---
|
|
220
|
-
|
|
221
|
-
## Quick Reference
|
|
135
|
+
| Need | Use Instead |
|
|
136
|
+
|------|-------------|
|
|
137
|
+
| Transform fields | Set node with expressions |
|
|
138
|
+
| Filter items | Filter node or Switch |
|
|
139
|
+
| Merge data | Merge node |
|
|
140
|
+
| Loop | n8n processes arrays natively |
|
|
141
|
+
| Date formatting | `{{ $now.format('yyyy-MM-dd') }}` |
|
|
222
142
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
143
|
+
When code IS necessary:
|
|
144
|
+
- Re-establishing `pairedItem` after chain breaks
|
|
145
|
+
- Complex conditional logic
|
|
146
|
+
- API parsing expressions can't handle
|
|
226
147
|
|
|
227
|
-
|
|
228
|
-
{{ $env.VAR_NAME }}
|
|
148
|
+
## Pre-Edit Checklist
|
|
229
149
|
|
|
230
|
-
|
|
231
|
-
|
|
150
|
+
| Step | Why |
|
|
151
|
+
|------|-----|
|
|
152
|
+
| 1. Get explicit user approval | Don't surprise |
|
|
153
|
+
| 2. List versions | Know rollback point |
|
|
154
|
+
| 3. Read full workflow | Understand current state |
|
|
155
|
+
| 4. Make targeted change | Minimal surface area |
|
|
232
156
|
|
|
233
|
-
|
|
234
|
-
{{ $('lookup').all().some(i => i.json.id === $json.id) }}
|
|
157
|
+
> **Note:** Validation and cleanup are now automatic. Every create/update validates, auto-fixes, and formats automatically.
|
|
235
158
|
|
|
236
|
-
|
|
237
|
-
{{ $now.format('yyyy-MM-dd') }}
|
|
159
|
+
## Node-Specific Settings
|
|
238
160
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
161
|
+
See [Node Config](node-config.md) for:
|
|
162
|
+
- Resource locator (`__rl`) format with `cachedResultName`
|
|
163
|
+
- Google Sheets, Gmail, Discord required fields
|
|
164
|
+
- Set node JSON mode vs manual mapping
|
|
165
|
+
- Error handling options
|