Skip to main content
RapidDev - Software Development Agency
n8n-tutorial

How to Store Workflow Data in n8n

Store persistent data in n8n workflows using $getWorkflowStaticData('global') for workflow-level storage or $getWorkflowStaticData('node') for node-level storage. Static data persists across executions and is ideal for counters, timestamps, tokens, and deduplication keys. For larger datasets, connect n8n to an external database like PostgreSQL or MySQL.

What you'll learn

  • How to use $getWorkflowStaticData for persistent key-value storage
  • The difference between global and node-scoped static data
  • How to store and retrieve complex data structures
  • When to use static data versus an external database
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner8 min read15 minutesn8n 1.0+ (self-hosted, Docker, and n8n Cloud)March 2026RapidDev Engineering Team
TL;DR

Store persistent data in n8n workflows using $getWorkflowStaticData('global') for workflow-level storage or $getWorkflowStaticData('node') for node-level storage. Static data persists across executions and is ideal for counters, timestamps, tokens, and deduplication keys. For larger datasets, connect n8n to an external database like PostgreSQL or MySQL.

Why Store Workflow Data in n8n

By default, each n8n workflow execution is stateless — data from one execution is not available to the next. But many automation tasks need to remember state between runs: tracking the last processed record ID, storing an API refresh token, counting events, or deduplicating items. n8n provides built-in static data storage that persists across executions without needing an external database. For larger datasets or complex queries, you can connect n8n to PostgreSQL, MySQL, or other databases using dedicated nodes.

Prerequisites

  • A running n8n instance (self-hosted or n8n Cloud)
  • A workflow where you need to persist data between executions
  • Basic familiarity with JavaScript expressions in n8n
  • For database storage: credentials for PostgreSQL, MySQL, or another supported database

Step-by-step guide

1

Access workflow static data in a Code node

The $getWorkflowStaticData function returns a JavaScript object that persists across workflow executions. Use 'global' scope to share data across all nodes in the workflow, or 'node' scope for data specific to a single node. The returned object behaves like a regular JavaScript object — you can read and write properties, and changes are automatically saved when the execution completes. Add a Code node to your workflow and use $getWorkflowStaticData to access persistent storage.

typescript
1// Access global static data (shared across all nodes)
2const staticData = $getWorkflowStaticData('global');
3
4// Read a value (returns undefined if not set)
5const lastRunTime = staticData.lastRunTime;
6
7// Write a value (automatically saved after execution)
8staticData.lastRunTime = new Date().toISOString();
9staticData.runCount = (staticData.runCount || 0) + 1;
10
11return [{
12 json: {
13 lastRunTime: lastRunTime || 'First run',
14 currentRunTime: staticData.lastRunTime,
15 totalRuns: staticData.runCount
16 }
17}];

Expected result: The first execution shows lastRunTime as 'First run'. Subsequent executions show the timestamp from the previous run, proving data persists.

2

Track the last processed record for incremental syncs

A common use case for static data is tracking the last processed record ID or timestamp for incremental data syncing. Instead of processing all records every time, store the ID of the last processed record and use it to filter subsequent queries. This pattern works with any data source: APIs, databases, or file systems.

typescript
1// Code node: Track last processed ID for incremental sync
2const staticData = $getWorkflowStaticData('global');
3const lastProcessedId = staticData.lastProcessedId || 0;
4
5// Get all items from the previous node
6const items = $input.all();
7
8// Filter to only new items (ID greater than last processed)
9const newItems = items.filter(item => item.json.id > lastProcessedId);
10
11// Update the last processed ID
12if (newItems.length > 0) {
13 const maxId = Math.max(...newItems.map(item => item.json.id));
14 staticData.lastProcessedId = maxId;
15}
16
17return newItems.length > 0 ? newItems : [{
18 json: { message: 'No new items to process' }
19}];

Expected result: The first run processes all items. Subsequent runs only process items with IDs greater than the last saved ID.

3

Use node-scoped static data

When different nodes in the same workflow need their own separate storage, use node-scoped static data with $getWorkflowStaticData('node'). Each node gets its own independent storage space. This prevents naming collisions when multiple nodes store data with the same key names. Node-scoped data is tied to the specific node instance, so renaming or replacing the node creates a new storage space.

typescript
1// Node-scoped static data — each node has its own storage
2const nodeData = $getWorkflowStaticData('node');
3
4// This 'counter' is independent from any other node's 'counter'
5nodeData.counter = (nodeData.counter || 0) + 1;
6nodeData.lastSeen = new Date().toISOString();
7
8return [{
9 json: {
10 nodeCounter: nodeData.counter,
11 nodeLastSeen: nodeData.lastSeen
12 }
13}];

Expected result: The counter increments independently for this specific node. Other nodes using the same key name have separate values.

4

Store complex data structures

Static data supports any JSON-serializable value: strings, numbers, booleans, arrays, and nested objects. This lets you store configuration, lookup tables, caches, and state machines. Be mindful of size — static data is stored in the n8n database alongside the workflow, so keep it under a few kilobytes. For large datasets, use an external database instead.

typescript
1// Store complex data structures in static data
2const staticData = $getWorkflowStaticData('global');
3
4// Initialize a deduplication cache
5if (!staticData.processedEmails) {
6 staticData.processedEmails = {};
7}
8
9// Check and update the cache
10const items = $input.all();
11const newItems = [];
12
13for (const item of items) {
14 const email = item.json.email;
15 if (!staticData.processedEmails[email]) {
16 staticData.processedEmails[email] = {
17 firstSeen: new Date().toISOString(),
18 count: 1
19 };
20 newItems.push(item);
21 } else {
22 staticData.processedEmails[email].count += 1;
23 }
24}
25
26// Clean up old entries (keep only last 1000)
27const emails = Object.keys(staticData.processedEmails);
28if (emails.length > 1000) {
29 const toRemove = emails.slice(0, emails.length - 1000);
30 toRemove.forEach(e => delete staticData.processedEmails[e]);
31}
32
33return newItems.length > 0 ? newItems : [{ json: { message: 'All items already processed' } }];

Expected result: Duplicate emails are filtered out across executions. The cache stores when each email was first seen and how many times it appeared.

5

Use database nodes for larger storage needs

When your data exceeds what static data can handle (roughly a few KB), connect n8n to an external database. Use the PostgreSQL, MySQL, or MongoDB nodes to read and write data. Create a dedicated table for your workflow state and use it to store records, logs, or large lookup datasets. This approach is better for structured data, complex queries, and data shared across multiple workflows.

typescript
1// PostgreSQL node configuration for storing workflow state
2// Operation: Execute Query
3// Query:
4
5-- Create a state table (run once)
6CREATE TABLE IF NOT EXISTS workflow_state (
7 workflow_id VARCHAR(50) PRIMARY KEY,
8 state_key VARCHAR(100) NOT NULL,
9 state_value JSONB NOT NULL,
10 updated_at TIMESTAMP DEFAULT NOW()
11);
12
13-- Upsert state data
14INSERT INTO workflow_state (workflow_id, state_key, state_value, updated_at)
15VALUES ('my-sync-workflow', 'last_cursor', '"abc123"'::jsonb, NOW())
16ON CONFLICT (workflow_id)
17DO UPDATE SET state_value = EXCLUDED.state_value, updated_at = NOW();
18
19-- Read state data
20SELECT state_value FROM workflow_state WHERE workflow_id = 'my-sync-workflow';

Expected result: Workflow state is stored in PostgreSQL and persists independently of n8n. Multiple workflows can share state through the database.

Complete working example

incremental-sync-code-node.js
1// Code node: Complete incremental sync with static data
2// Place this after a node that fetches all records (e.g., HTTP Request)
3
4const staticData = $getWorkflowStaticData('global');
5
6// Initialize state on first run
7if (!staticData.lastProcessedId) {
8 staticData.lastProcessedId = 0;
9}
10if (!staticData.totalProcessed) {
11 staticData.totalProcessed = 0;
12}
13if (!staticData.lastRunTimestamp) {
14 staticData.lastRunTimestamp = null;
15}
16
17const previousLastId = staticData.lastProcessedId;
18const items = $input.all();
19
20// Filter to only new items since last run
21const newItems = items.filter(item => {
22 return item.json.id > previousLastId;
23});
24
25// Sort by ID to ensure we process in order
26newItems.sort((a, b) => a.json.id - b.json.id);
27
28// Update tracking state
29if (newItems.length > 0) {
30 const maxId = Math.max(...newItems.map(item => item.json.id));
31 staticData.lastProcessedId = maxId;
32 staticData.totalProcessed += newItems.length;
33}
34staticData.lastRunTimestamp = new Date().toISOString();
35
36// Add metadata to each item
37const enrichedItems = newItems.map(item => ({
38 json: {
39 ...item.json,
40 _syncMetadata: {
41 processedAt: staticData.lastRunTimestamp,
42 batchSize: newItems.length,
43 totalProcessed: staticData.totalProcessed
44 }
45 }
46}));
47
48// Return new items or a summary if none
49if (enrichedItems.length > 0) {
50 return enrichedItems;
51}
52
53return [{
54 json: {
55 message: 'No new items since last run',
56 lastProcessedId: staticData.lastProcessedId,
57 lastRunTimestamp: staticData.lastRunTimestamp,
58 totalProcessed: staticData.totalProcessed
59 }
60}];

Common mistakes when storing Workflow Data in n8n

Why it's a problem: Storing large datasets in static data instead of an external database

How to avoid: Static data is stored in the n8n database with the workflow. Keep it under a few KB. Use PostgreSQL or MySQL nodes for larger data.

Why it's a problem: Updating the last processed ID before processing items

How to avoid: Always update tracking state after successful processing. If the workflow fails mid-execution, the unchanged state ensures items are retried.

Why it's a problem: Using $getWorkflowStaticData in expression fields instead of Code nodes

How to avoid: $getWorkflowStaticData is available in expressions but writing to it reliably requires a Code node. Use Code nodes for read-write operations.

Why it's a problem: Expecting static data to persist after deleting and recreating a workflow

How to avoid: Static data is tied to the workflow ID. Deleting the workflow deletes its static data. Export the workflow before deleting to preserve the data.

Best practices

  • Use global static data for state shared across all nodes in a workflow
  • Use node-scoped static data when multiple nodes need independent storage with the same key names
  • Keep static data small — store only IDs, timestamps, and counters, not full records
  • Clean up old entries in static data to prevent unbounded growth over time
  • Update state after successful processing, not before, to prevent data loss on failures
  • Use an external database for datasets larger than a few kilobytes or for cross-workflow state
  • Document what each static data key stores by using descriptive key names
  • Test static data behavior by running the workflow multiple times manually

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I need my n8n workflow to remember the last processed record ID between executions so it only processes new records each time. How do I use $getWorkflowStaticData for incremental syncing?

n8n Prompt

Help me build an n8n workflow that stores a deduplication cache using static data to prevent processing the same email address twice across multiple workflow runs.

Frequently asked questions

Where is static data stored?

Static data is stored in the n8n database (SQLite by default or PostgreSQL if configured) alongside the workflow definition. It persists as long as the workflow exists.

Is there a size limit for static data?

There is no hard limit, but static data is loaded into memory on every execution. Keep it under a few kilobytes for best performance. Use an external database for larger datasets.

Can I access static data from one workflow in another workflow?

No. Static data is scoped to a single workflow. To share data between workflows, use an external database, a file, or the Execute Workflow node to pass data directly.

Does static data persist after n8n restarts?

Yes. Static data is saved to the database, so it persists across n8n restarts, container recreations, and server reboots.

How do I clear static data for a workflow?

Add a Code node that sets all keys to undefined or an empty object: const data = $getWorkflowStaticData('global'); Object.keys(data).forEach(key => delete data[key]);. Run the workflow once to clear the data.

Can I view static data without running the workflow?

Static data is not visible in the n8n UI. To inspect it, add a Code node that reads and returns the static data object, then run the workflow manually.

Can RapidDev help me design data persistence for my n8n workflows?

Yes. RapidDev can architect workflow state management using static data, external databases, and caching strategies for your specific use case. Contact RapidDev for a free consultation.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.