# How to Store Workflow Data in n8n

- Tool: n8n
- Difficulty: Beginner
- Time required: 15 minutes
- Compatibility: n8n 1.0+ (self-hosted, Docker, and n8n Cloud)
- Last updated: March 2026

## 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.

## Before you start

- 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.

```
// Access global static data (shared across all nodes)
const staticData = $getWorkflowStaticData('global');

// Read a value (returns undefined if not set)
const lastRunTime = staticData.lastRunTime;

// Write a value (automatically saved after execution)
staticData.lastRunTime = new Date().toISOString();
staticData.runCount = (staticData.runCount || 0) + 1;

return [{
  json: {
    lastRunTime: lastRunTime || 'First run',
    currentRunTime: staticData.lastRunTime,
    totalRuns: staticData.runCount
  }
}];
```

**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.

```
// Code node: Track last processed ID for incremental sync
const staticData = $getWorkflowStaticData('global');
const lastProcessedId = staticData.lastProcessedId || 0;

// Get all items from the previous node
const items = $input.all();

// Filter to only new items (ID greater than last processed)
const newItems = items.filter(item => item.json.id > lastProcessedId);

// Update the last processed ID
if (newItems.length > 0) {
  const maxId = Math.max(...newItems.map(item => item.json.id));
  staticData.lastProcessedId = maxId;
}

return newItems.length > 0 ? newItems : [{
  json: { message: 'No new items to process' }
}];
```

**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.

```
// Node-scoped static data — each node has its own storage
const nodeData = $getWorkflowStaticData('node');

// This 'counter' is independent from any other node's 'counter'
nodeData.counter = (nodeData.counter || 0) + 1;
nodeData.lastSeen = new Date().toISOString();

return [{
  json: {
    nodeCounter: nodeData.counter,
    nodeLastSeen: nodeData.lastSeen
  }
}];
```

**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.

```
// Store complex data structures in static data
const staticData = $getWorkflowStaticData('global');

// Initialize a deduplication cache
if (!staticData.processedEmails) {
  staticData.processedEmails = {};
}

// Check and update the cache
const items = $input.all();
const newItems = [];

for (const item of items) {
  const email = item.json.email;
  if (!staticData.processedEmails[email]) {
    staticData.processedEmails[email] = {
      firstSeen: new Date().toISOString(),
      count: 1
    };
    newItems.push(item);
  } else {
    staticData.processedEmails[email].count += 1;
  }
}

// Clean up old entries (keep only last 1000)
const emails = Object.keys(staticData.processedEmails);
if (emails.length > 1000) {
  const toRemove = emails.slice(0, emails.length - 1000);
  toRemove.forEach(e => delete staticData.processedEmails[e]);
}

return 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.

```
// PostgreSQL node configuration for storing workflow state
// Operation: Execute Query
// Query:

-- Create a state table (run once)
CREATE TABLE IF NOT EXISTS workflow_state (
  workflow_id VARCHAR(50) PRIMARY KEY,
  state_key VARCHAR(100) NOT NULL,
  state_value JSONB NOT NULL,
  updated_at TIMESTAMP DEFAULT NOW()
);

-- Upsert state data
INSERT INTO workflow_state (workflow_id, state_key, state_value, updated_at)
VALUES ('my-sync-workflow', 'last_cursor', '"abc123"'::jsonb, NOW())
ON CONFLICT (workflow_id)
DO UPDATE SET state_value = EXCLUDED.state_value, updated_at = NOW();

-- Read state data
SELECT 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 code example

File: `incremental-sync-code-node.js`

```javascript
// Code node: Complete incremental sync with static data
// Place this after a node that fetches all records (e.g., HTTP Request)

const staticData = $getWorkflowStaticData('global');

// Initialize state on first run
if (!staticData.lastProcessedId) {
  staticData.lastProcessedId = 0;
}
if (!staticData.totalProcessed) {
  staticData.totalProcessed = 0;
}
if (!staticData.lastRunTimestamp) {
  staticData.lastRunTimestamp = null;
}

const previousLastId = staticData.lastProcessedId;
const items = $input.all();

// Filter to only new items since last run
const newItems = items.filter(item => {
  return item.json.id > previousLastId;
});

// Sort by ID to ensure we process in order
newItems.sort((a, b) => a.json.id - b.json.id);

// Update tracking state
if (newItems.length > 0) {
  const maxId = Math.max(...newItems.map(item => item.json.id));
  staticData.lastProcessedId = maxId;
  staticData.totalProcessed += newItems.length;
}
staticData.lastRunTimestamp = new Date().toISOString();

// Add metadata to each item
const enrichedItems = newItems.map(item => ({
  json: {
    ...item.json,
    _syncMetadata: {
      processedAt: staticData.lastRunTimestamp,
      batchSize: newItems.length,
      totalProcessed: staticData.totalProcessed
    }
  }
}));

// Return new items or a summary if none
if (enrichedItems.length > 0) {
  return enrichedItems;
}

return [{
  json: {
    message: 'No new items since last run',
    lastProcessedId: staticData.lastProcessedId,
    lastRunTimestamp: staticData.lastRunTimestamp,
    totalProcessed: staticData.totalProcessed
  }
}];
```

## Common mistakes

- **Storing large datasets in static data instead of an external database** — undefined Fix: 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.
- **Updating the last processed ID before processing items** — undefined Fix: Always update tracking state after successful processing. If the workflow fails mid-execution, the unchanged state ensures items are retried.
- **Using $getWorkflowStaticData in expression fields instead of Code nodes** — undefined Fix: $getWorkflowStaticData is available in expressions but writing to it reliably requires a Code node. Use Code nodes for read-write operations.
- **Expecting static data to persist after deleting and recreating a workflow** — undefined Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/n8n-tutorial/how-to-store-workflow-data-in-n8n
© RapidDev — https://www.rapidevelopers.com/n8n-tutorial/how-to-store-workflow-data-in-n8n
