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
- ## Output modes
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, not shared globally across all workflows
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
- Future versions of this node may add built-in Redis, Postgres, or n8n Data Tables backends.
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 workflow-global and node-local variables',
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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-variable",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Local and global scoped variables for n8n workflows",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",