n8n-nodes-variable 1.0.7 → 1.0.8

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 SQLite database 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 SQLite database file on the n8n host at:
101
+
102
+ ```
103
+ ${N8N_USER_FOLDER ?? ~/.n8n}/n8n-nodes-variable.db
104
+ ```
105
+
106
+ The database and table are **created automatically on first use** — no setup required. Data survives instance restarts and is accessible from any workflow on the same n8n instance. This is ideal for cross-workflow counters, shared feature flags, or any state that multiple workflows need to read and write.
107
+
108
+ > **Note:** the database 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,16 @@
1
+ /**
2
+ * Cross-workflow variable storage backed by a local SQLite database.
3
+ *
4
+ * The database is auto-created at:
5
+ * ${N8N_USER_FOLDER ?? ~/.n8n}/n8n-nodes-variable.db
6
+ *
7
+ * No configuration is required — the database file and table are created
8
+ * automatically on first use and shared across all workflows on the instance.
9
+ */
10
+ import type { StoredVariableEntry } from './types';
11
+ export declare function dbGetVariable(namespace: string, key: string): StoredVariableEntry | undefined;
12
+ export declare function dbSetVariable(namespace: string, key: string, value: unknown, typeName: string, includeMetadata: boolean): void;
13
+ export declare function dbDeleteVariable(namespace: string, key: string): boolean;
14
+ export declare function dbHasVariable(namespace: string, key: string): boolean;
15
+ export declare function dbListVariables(namespace: string): Record<string, StoredVariableEntry>;
16
+ export declare function dbClearNamespace(namespace: string): number;
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ /**
3
+ * Cross-workflow variable storage backed by a local SQLite database.
4
+ *
5
+ * The database is auto-created at:
6
+ * ${N8N_USER_FOLDER ?? ~/.n8n}/n8n-nodes-variable.db
7
+ *
8
+ * No configuration is required — the database file and table are created
9
+ * automatically on first use and shared across all workflows on the instance.
10
+ */
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.dbGetVariable = dbGetVariable;
16
+ exports.dbSetVariable = dbSetVariable;
17
+ exports.dbDeleteVariable = dbDeleteVariable;
18
+ exports.dbHasVariable = dbHasVariable;
19
+ exports.dbListVariables = dbListVariables;
20
+ exports.dbClearNamespace = dbClearNamespace;
21
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
22
+ const fs_1 = __importDefault(require("fs"));
23
+ const os_1 = __importDefault(require("os"));
24
+ const path_1 = __importDefault(require("path"));
25
+ const DB_FILENAME = 'n8n-nodes-variable.db';
26
+ function getDbPath() {
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 we can't create the n8n folder, fall back to the OS temp directory
35
+ return path_1.default.join(os_1.default.tmpdir(), DB_FILENAME);
36
+ }
37
+ return path_1.default.join(userFolder, DB_FILENAME);
38
+ }
39
+ // Singleton connection — reused across all node executions in the same process
40
+ let _db = null;
41
+ function getDb() {
42
+ if (_db)
43
+ return _db;
44
+ const dbPath = getDbPath();
45
+ _db = new better_sqlite3_1.default(dbPath);
46
+ // WAL journal mode allows concurrent reads alongside writes
47
+ _db.pragma('journal_mode = WAL');
48
+ _db.pragma('foreign_keys = ON');
49
+ // Auto-create the table if it doesn't exist
50
+ _db.exec(`
51
+ CREATE TABLE IF NOT EXISTS variables (
52
+ namespace TEXT NOT NULL,
53
+ key TEXT NOT NULL,
54
+ value TEXT NOT NULL,
55
+ type TEXT,
56
+ created_at TEXT,
57
+ updated_at TEXT,
58
+ PRIMARY KEY (namespace, key)
59
+ )
60
+ `);
61
+ return _db;
62
+ }
63
+ // ─── CRUD operations ─────────────────────────────────────────────────────────
64
+ function dbGetVariable(namespace, key) {
65
+ const db = getDb();
66
+ const row = db
67
+ .prepare('SELECT value, type, created_at, updated_at FROM variables WHERE namespace = ? AND key = ?')
68
+ .get(namespace, key);
69
+ if (!row)
70
+ return undefined;
71
+ return {
72
+ value: JSON.parse(row.value),
73
+ type: row.type ?? undefined,
74
+ createdAt: row.created_at ?? undefined,
75
+ updatedAt: row.updated_at ?? undefined,
76
+ };
77
+ }
78
+ function dbSetVariable(namespace, key, value, typeName, includeMetadata) {
79
+ const db = getDb();
80
+ const now = new Date().toISOString();
81
+ const serialized = JSON.stringify(value);
82
+ if (includeMetadata) {
83
+ // Preserve the original created_at on conflict
84
+ db.prepare(`INSERT INTO variables (namespace, key, value, type, created_at, updated_at)
85
+ VALUES (?, ?, ?, ?, ?, ?)
86
+ ON CONFLICT(namespace, key) DO UPDATE SET
87
+ value = excluded.value,
88
+ type = excluded.type,
89
+ updated_at = excluded.updated_at`).run(namespace, key, serialized, typeName, now, now);
90
+ }
91
+ else {
92
+ db.prepare(`INSERT INTO variables (namespace, key, value, type, created_at, updated_at)
93
+ VALUES (?, ?, ?, NULL, NULL, NULL)
94
+ ON CONFLICT(namespace, key) DO UPDATE SET
95
+ value = excluded.value,
96
+ type = NULL,
97
+ created_at = NULL,
98
+ updated_at = NULL`).run(namespace, key, serialized);
99
+ }
100
+ }
101
+ function dbDeleteVariable(namespace, key) {
102
+ const db = getDb();
103
+ const result = db
104
+ .prepare('DELETE FROM variables WHERE namespace = ? AND key = ?')
105
+ .run(namespace, key);
106
+ return result.changes > 0;
107
+ }
108
+ function dbHasVariable(namespace, key) {
109
+ const db = getDb();
110
+ const row = db
111
+ .prepare('SELECT 1 FROM variables WHERE namespace = ? AND key = ? LIMIT 1')
112
+ .get(namespace, key);
113
+ return row !== undefined;
114
+ }
115
+ function dbListVariables(namespace) {
116
+ const db = getDb();
117
+ const rows = db
118
+ .prepare('SELECT key, value, type, created_at, updated_at FROM variables WHERE namespace = ? ORDER BY key')
119
+ .all(namespace);
120
+ const result = {};
121
+ for (const row of rows) {
122
+ result[row.key] = {
123
+ value: JSON.parse(row.value),
124
+ type: row.type ?? undefined,
125
+ createdAt: row.created_at ?? undefined,
126
+ updatedAt: row.updated_at ?? undefined,
127
+ };
128
+ }
129
+ return result;
130
+ }
131
+ function dbClearNamespace(namespace) {
132
+ const db = getDb();
133
+ const result = db
134
+ .prepare('DELETE FROM variables WHERE namespace = ?')
135
+ .run(namespace);
136
+ return result.changes;
137
+ }
@@ -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.8",
4
4
  "description": "Local and global scoped variables for n8n workflows",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",
@@ -38,6 +38,7 @@
38
38
  ]
39
39
  },
40
40
  "devDependencies": {
41
+ "@types/better-sqlite3": "^7.6.11",
41
42
  "@types/jest": "^29.5.12",
42
43
  "@types/node": "^18.19.50",
43
44
  "@typescript-eslint/eslint-plugin": "^6.21.0",
@@ -54,6 +55,9 @@
54
55
  "peerDependencies": {
55
56
  "n8n-workflow": "*"
56
57
  },
58
+ "dependencies": {
59
+ "better-sqlite3": "^9.4.0"
60
+ },
57
61
  "jest": {
58
62
  "preset": "ts-jest",
59
63
  "testEnvironment": "node",