n8n-nodes-variable 1.0.7 → 1.0.9
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/README.md
CHANGED
|
@@ -12,6 +12,7 @@ The **Variable** node lets you store, retrieve, update, and delete named variabl
|
|
|
12
12
|
- **Workflow Global** — persists across executions using n8n's workflow static data
|
|
13
13
|
- **Node Local** — persists for the specific node instance
|
|
14
14
|
- **Custom Namespace** — workflow global storage with a fully dynamic namespace string (great for per-user / per-guild data)
|
|
15
|
+
- **Cross-Workflow (Shared)** — variables are stored in a local JSON file and shared across **all** workflows on this n8n instance
|
|
15
16
|
|
|
16
17
|
---
|
|
17
18
|
|
|
@@ -94,6 +95,18 @@ cooldowns → command cooldown per guild+user
|
|
|
94
95
|
guild_{{$json.guild.id}} → per-guild settings
|
|
95
96
|
```
|
|
96
97
|
|
|
98
|
+
### Cross-Workflow (Shared)
|
|
99
|
+
|
|
100
|
+
Variables are stored in a JSON file on the n8n host at:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
${N8N_USER_FOLDER ?? ~/.n8n}/n8n-nodes-variable-data.json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The file is **created automatically on first use** — no setup or dependencies required. Writes are performed atomically (write to a temporary file, then rename) to prevent corruption. Data survives instance restarts and is accessible from any workflow on the same n8n instance.
|
|
107
|
+
|
|
108
|
+
> **Note:** the file lives on the n8n host machine. If you run n8n in a container or cloud environment, ensure the `.n8n` data directory is persisted to a volume so data is not lost on container restarts.
|
|
109
|
+
|
|
97
110
|
---
|
|
98
111
|
|
|
99
112
|
## Examples
|
|
@@ -178,7 +191,28 @@ The variable `stats.runCount` persists between executions and increments each ti
|
|
|
178
191
|
|
|
179
192
|
---
|
|
180
193
|
|
|
181
|
-
|
|
194
|
+
### 5. Cross-workflow shared counter
|
|
195
|
+
|
|
196
|
+
Count total webhook hits across every workflow on the instance:
|
|
197
|
+
|
|
198
|
+
**In each workflow that receives a webhook — Increment shared counter:**
|
|
199
|
+
- Operation: `Increment Variable`
|
|
200
|
+
- Scope: `Cross-Workflow (Shared)`
|
|
201
|
+
- Namespace: `global_stats`
|
|
202
|
+
- Key: `webhook_hits`
|
|
203
|
+
- Amount: `1`
|
|
204
|
+
- Initialize If Missing: `true`
|
|
205
|
+
- Initial Value: `0`
|
|
206
|
+
|
|
207
|
+
**In a reporting workflow — Read the counter:**
|
|
208
|
+
- Operation: `Get Variable`
|
|
209
|
+
- Scope: `Cross-Workflow (Shared)`
|
|
210
|
+
- Namespace: `global_stats`
|
|
211
|
+
- Key: `webhook_hits`
|
|
212
|
+
|
|
213
|
+
Because the database is shared, all workflows see and update the same value.
|
|
214
|
+
|
|
215
|
+
---
|
|
182
216
|
|
|
183
217
|
| Mode | Description |
|
|
184
218
|
|---|---|
|
|
@@ -209,11 +243,9 @@ Workflow Global and Node Local variables use n8n's static data, which is:
|
|
|
209
243
|
|
|
210
244
|
- **Suitable for:** counters, feature flags, small state objects, per-user values in low-traffic bots
|
|
211
245
|
- **Not suitable for:** high-concurrency write operations (e.g., simultaneously updating the same counter from hundreds of parallel executions)
|
|
212
|
-
- **Not cross-workflow:** variables are scoped to the workflow they belong to
|
|
213
|
-
|
|
214
|
-
For high-volume or high-concurrency state, consider using a database node (Redis, Postgres, MongoDB) instead.
|
|
246
|
+
- **Not cross-workflow:** Workflow Global and Node Local variables are scoped to the workflow they belong to. Use the **Cross-Workflow (Shared)** scope to share state across workflows.
|
|
215
247
|
|
|
216
|
-
|
|
248
|
+
For high-volume or high-concurrency state, consider using a dedicated database node (Redis, Postgres, MongoDB) instead.
|
|
217
249
|
|
|
218
250
|
---
|
|
219
251
|
|
|
@@ -4,6 +4,7 @@ exports.Variable = void 0;
|
|
|
4
4
|
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
5
|
const valueParser_1 = require("./helpers/valueParser");
|
|
6
6
|
const storage_1 = require("./helpers/storage");
|
|
7
|
+
const dbStorage_1 = require("./helpers/dbStorage");
|
|
7
8
|
class Variable {
|
|
8
9
|
constructor() {
|
|
9
10
|
this.description = {
|
|
@@ -68,6 +69,11 @@ class Variable {
|
|
|
68
69
|
value: 'customNamespace',
|
|
69
70
|
description: 'Workflow global storage with a custom namespace expression.',
|
|
70
71
|
},
|
|
72
|
+
{
|
|
73
|
+
name: 'Cross-Workflow (Shared)',
|
|
74
|
+
value: 'crossWorkflow',
|
|
75
|
+
description: 'Variables are stored in a shared local database file and are accessible across ALL workflows on this instance.',
|
|
76
|
+
},
|
|
71
77
|
],
|
|
72
78
|
default: 'workflowGlobal',
|
|
73
79
|
description: 'Where to store the variable',
|
|
@@ -102,7 +108,7 @@ class Variable {
|
|
|
102
108
|
description: 'Namespace to organize variables. Supports expressions like economy_{{$json.guild.id}}.',
|
|
103
109
|
displayOptions: {
|
|
104
110
|
show: {
|
|
105
|
-
scope: ['workflowGlobal', 'nodeLocal'],
|
|
111
|
+
scope: ['workflowGlobal', 'nodeLocal', 'crossWorkflow'],
|
|
106
112
|
},
|
|
107
113
|
},
|
|
108
114
|
},
|
|
@@ -347,9 +353,9 @@ class Variable {
|
|
|
347
353
|
name: 'includeMetadata',
|
|
348
354
|
type: 'boolean',
|
|
349
355
|
default: false,
|
|
350
|
-
description: 'Whether to store and expose createdAt/updatedAt/type metadata for
|
|
356
|
+
description: 'Whether to store and expose createdAt/updatedAt/type metadata for variables',
|
|
351
357
|
displayOptions: {
|
|
352
|
-
show: { scope: ['workflowGlobal', 'nodeLocal', 'customNamespace'] },
|
|
358
|
+
show: { scope: ['workflowGlobal', 'nodeLocal', 'customNamespace', 'crossWorkflow'] },
|
|
353
359
|
},
|
|
354
360
|
},
|
|
355
361
|
],
|
|
@@ -410,6 +416,7 @@ function resolveNamespace(ctx, scope, i) {
|
|
|
410
416
|
// ─── Operation dispatcher ─────────────────────────────────────────────────────
|
|
411
417
|
function executeOperation(ctx, operation, scope, namespace, itemJson, i, includeMetadata) {
|
|
412
418
|
const isLocal = scope === 'localExecution';
|
|
419
|
+
const isDb = scope === 'crossWorkflow';
|
|
413
420
|
const storagePath = isLocal
|
|
414
421
|
? ctx.getNodeParameter('localStoragePath', i, '_variables')
|
|
415
422
|
: '';
|
|
@@ -417,6 +424,8 @@ function executeOperation(ctx, operation, scope, namespace, itemJson, i, include
|
|
|
417
424
|
const get = (key) => {
|
|
418
425
|
if (isLocal)
|
|
419
426
|
return (0, storage_1.localGetVariable)(itemJson, storagePath, namespace, key);
|
|
427
|
+
if (isDb)
|
|
428
|
+
return (0, dbStorage_1.dbGetVariable)(namespace, key)?.value;
|
|
420
429
|
const entry = (0, storage_1.staticGetVariable)(getStaticData(ctx, scope), namespace, key);
|
|
421
430
|
return entry?.value;
|
|
422
431
|
};
|
|
@@ -424,6 +433,9 @@ function executeOperation(ctx, operation, scope, namespace, itemJson, i, include
|
|
|
424
433
|
if (isLocal) {
|
|
425
434
|
(0, storage_1.localSetVariable)(itemJson, storagePath, namespace, key, value);
|
|
426
435
|
}
|
|
436
|
+
else if (isDb) {
|
|
437
|
+
(0, dbStorage_1.dbSetVariable)(namespace, key, value, typeName, includeMetadata);
|
|
438
|
+
}
|
|
427
439
|
else {
|
|
428
440
|
(0, storage_1.staticSetVariable)(getStaticData(ctx, scope), namespace, key, value, typeName, includeMetadata);
|
|
429
441
|
}
|
|
@@ -431,16 +443,28 @@ function executeOperation(ctx, operation, scope, namespace, itemJson, i, include
|
|
|
431
443
|
const del = (key) => {
|
|
432
444
|
if (isLocal)
|
|
433
445
|
return (0, storage_1.localDeleteVariable)(itemJson, storagePath, namespace, key);
|
|
446
|
+
if (isDb)
|
|
447
|
+
return (0, dbStorage_1.dbDeleteVariable)(namespace, key);
|
|
434
448
|
return (0, storage_1.staticDeleteVariable)(getStaticData(ctx, scope), namespace, key);
|
|
435
449
|
};
|
|
436
450
|
const has = (key) => {
|
|
437
451
|
if (isLocal)
|
|
438
452
|
return (0, storage_1.localHasVariable)(itemJson, storagePath, namespace, key);
|
|
453
|
+
if (isDb)
|
|
454
|
+
return (0, dbStorage_1.dbHasVariable)(namespace, key);
|
|
439
455
|
return (0, storage_1.staticHasVariable)(getStaticData(ctx, scope), namespace, key);
|
|
440
456
|
};
|
|
441
457
|
const list = () => {
|
|
442
458
|
if (isLocal)
|
|
443
459
|
return (0, storage_1.localListVariables)(itemJson, storagePath, namespace);
|
|
460
|
+
if (isDb) {
|
|
461
|
+
const raw = (0, dbStorage_1.dbListVariables)(namespace);
|
|
462
|
+
const result = {};
|
|
463
|
+
for (const [k, entry] of Object.entries(raw)) {
|
|
464
|
+
result[k] = entry.value;
|
|
465
|
+
}
|
|
466
|
+
return result;
|
|
467
|
+
}
|
|
444
468
|
const raw = (0, storage_1.staticListVariables)(getStaticData(ctx, scope), namespace);
|
|
445
469
|
// unwrap StoredVariableEntry to plain values
|
|
446
470
|
const result = {};
|
|
@@ -452,6 +476,8 @@ function executeOperation(ctx, operation, scope, namespace, itemJson, i, include
|
|
|
452
476
|
const clear = () => {
|
|
453
477
|
if (isLocal)
|
|
454
478
|
return (0, storage_1.localClearNamespace)(itemJson, storagePath, namespace);
|
|
479
|
+
if (isDb)
|
|
480
|
+
return (0, dbStorage_1.dbClearNamespace)(namespace);
|
|
455
481
|
return (0, storage_1.staticClearNamespace)(getStaticData(ctx, scope), namespace);
|
|
456
482
|
};
|
|
457
483
|
const scopeLabel = scope;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-workflow variable storage backed by a local JSON file.
|
|
3
|
+
*
|
|
4
|
+
* The file is auto-created at:
|
|
5
|
+
* ${N8N_USER_FOLDER ?? ~/.n8n}/n8n-nodes-variable-data.json
|
|
6
|
+
*
|
|
7
|
+
* No configuration is required. Writes are performed atomically (write to a
|
|
8
|
+
* temporary file, then rename) to prevent data corruption on unexpected
|
|
9
|
+
* process exit. No native dependencies are needed.
|
|
10
|
+
*/
|
|
11
|
+
import type { StoredVariableEntry } from './types';
|
|
12
|
+
export declare function dbGetVariable(namespace: string, key: string): StoredVariableEntry | undefined;
|
|
13
|
+
export declare function dbSetVariable(namespace: string, key: string, value: unknown, typeName: string, includeMetadata: boolean): void;
|
|
14
|
+
export declare function dbDeleteVariable(namespace: string, key: string): boolean;
|
|
15
|
+
export declare function dbHasVariable(namespace: string, key: string): boolean;
|
|
16
|
+
export declare function dbListVariables(namespace: string): Record<string, StoredVariableEntry>;
|
|
17
|
+
export declare function dbClearNamespace(namespace: string): number;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cross-workflow variable storage backed by a local JSON file.
|
|
4
|
+
*
|
|
5
|
+
* The file is auto-created at:
|
|
6
|
+
* ${N8N_USER_FOLDER ?? ~/.n8n}/n8n-nodes-variable-data.json
|
|
7
|
+
*
|
|
8
|
+
* No configuration is required. Writes are performed atomically (write to a
|
|
9
|
+
* temporary file, then rename) to prevent data corruption on unexpected
|
|
10
|
+
* process exit. No native dependencies are needed.
|
|
11
|
+
*/
|
|
12
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.dbGetVariable = dbGetVariable;
|
|
17
|
+
exports.dbSetVariable = dbSetVariable;
|
|
18
|
+
exports.dbDeleteVariable = dbDeleteVariable;
|
|
19
|
+
exports.dbHasVariable = dbHasVariable;
|
|
20
|
+
exports.dbListVariables = dbListVariables;
|
|
21
|
+
exports.dbClearNamespace = dbClearNamespace;
|
|
22
|
+
const fs_1 = __importDefault(require("fs"));
|
|
23
|
+
const os_1 = __importDefault(require("os"));
|
|
24
|
+
const path_1 = __importDefault(require("path"));
|
|
25
|
+
const DATA_FILENAME = 'n8n-nodes-variable-data.json';
|
|
26
|
+
function getDataPath() {
|
|
27
|
+
const userFolder = process.env['N8N_USER_FOLDER'] ?? path_1.default.join(os_1.default.homedir(), '.n8n');
|
|
28
|
+
try {
|
|
29
|
+
if (!fs_1.default.existsSync(userFolder)) {
|
|
30
|
+
fs_1.default.mkdirSync(userFolder, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// If the n8n user folder can't be created, fall back to the OS temp dir
|
|
35
|
+
return path_1.default.join(os_1.default.tmpdir(), DATA_FILENAME);
|
|
36
|
+
}
|
|
37
|
+
return path_1.default.join(userFolder, DATA_FILENAME);
|
|
38
|
+
}
|
|
39
|
+
function readData() {
|
|
40
|
+
const dataPath = getDataPath();
|
|
41
|
+
try {
|
|
42
|
+
const content = fs_1.default.readFileSync(dataPath, 'utf-8');
|
|
43
|
+
const parsed = JSON.parse(content);
|
|
44
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// File doesn't exist yet or is corrupt — start fresh
|
|
50
|
+
}
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
function writeData(data) {
|
|
54
|
+
const dataPath = getDataPath();
|
|
55
|
+
const tmpPath = `${dataPath}.tmp`;
|
|
56
|
+
fs_1.default.writeFileSync(tmpPath, JSON.stringify(data), 'utf-8');
|
|
57
|
+
fs_1.default.renameSync(tmpPath, dataPath);
|
|
58
|
+
}
|
|
59
|
+
// ─── CRUD operations ─────────────────────────────────────────────────────────
|
|
60
|
+
function dbGetVariable(namespace, key) {
|
|
61
|
+
const data = readData();
|
|
62
|
+
const entry = data[namespace]?.[key];
|
|
63
|
+
if (!entry)
|
|
64
|
+
return undefined;
|
|
65
|
+
return {
|
|
66
|
+
value: entry.value,
|
|
67
|
+
type: entry.type,
|
|
68
|
+
createdAt: entry.created_at,
|
|
69
|
+
updatedAt: entry.updated_at,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function dbSetVariable(namespace, key, value, typeName, includeMetadata) {
|
|
73
|
+
const data = readData();
|
|
74
|
+
if (!data[namespace]) {
|
|
75
|
+
data[namespace] = {};
|
|
76
|
+
}
|
|
77
|
+
const now = new Date().toISOString();
|
|
78
|
+
if (includeMetadata) {
|
|
79
|
+
const existing = data[namespace][key];
|
|
80
|
+
data[namespace][key] = {
|
|
81
|
+
value,
|
|
82
|
+
type: typeName,
|
|
83
|
+
// Preserve original created_at on updates
|
|
84
|
+
created_at: existing?.created_at ?? now,
|
|
85
|
+
updated_at: now,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
data[namespace][key] = { value };
|
|
90
|
+
}
|
|
91
|
+
writeData(data);
|
|
92
|
+
}
|
|
93
|
+
function dbDeleteVariable(namespace, key) {
|
|
94
|
+
const data = readData();
|
|
95
|
+
if (!data[namespace] || !(key in data[namespace])) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
delete data[namespace][key];
|
|
99
|
+
writeData(data);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
function dbHasVariable(namespace, key) {
|
|
103
|
+
const data = readData();
|
|
104
|
+
return Object.prototype.hasOwnProperty.call(data[namespace] ?? {}, key);
|
|
105
|
+
}
|
|
106
|
+
function dbListVariables(namespace) {
|
|
107
|
+
const data = readData();
|
|
108
|
+
const ns = data[namespace] ?? {};
|
|
109
|
+
const result = {};
|
|
110
|
+
for (const [k, entry] of Object.entries(ns)) {
|
|
111
|
+
result[k] = {
|
|
112
|
+
value: entry.value,
|
|
113
|
+
type: entry.type,
|
|
114
|
+
createdAt: entry.created_at,
|
|
115
|
+
updatedAt: entry.updated_at,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
function dbClearNamespace(namespace) {
|
|
121
|
+
const data = readData();
|
|
122
|
+
const count = Object.keys(data[namespace] ?? {}).length;
|
|
123
|
+
if (count > 0) {
|
|
124
|
+
delete data[namespace];
|
|
125
|
+
writeData(data);
|
|
126
|
+
}
|
|
127
|
+
return count;
|
|
128
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type VariableScope = 'localExecution' | 'workflowGlobal' | 'nodeLocal' | 'customNamespace';
|
|
1
|
+
export type VariableScope = 'localExecution' | 'workflowGlobal' | 'nodeLocal' | 'customNamespace' | 'crossWorkflow';
|
|
2
2
|
export type VariableOperation = 'set' | 'get' | 'delete' | 'has' | 'list' | 'clear' | 'increment' | 'decrement' | 'appendToArray' | 'mergeObject' | 'toggleBoolean';
|
|
3
3
|
export type ValueType = 'string' | 'number' | 'boolean' | 'json' | 'array' | 'object' | 'auto';
|
|
4
4
|
export type OutputMode = 'preserveAndAdd' | 'resultOnly' | 'addField';
|