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
Access workflow static data in a Code node
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.
1// Access global static data (shared across all nodes)2const staticData = $getWorkflowStaticData('global');34// Read a value (returns undefined if not set)5const lastRunTime = staticData.lastRunTime;67// Write a value (automatically saved after execution)8staticData.lastRunTime = new Date().toISOString();9staticData.runCount = (staticData.runCount || 0) + 1;1011return [{12 json: {13 lastRunTime: lastRunTime || 'First run',14 currentRunTime: staticData.lastRunTime,15 totalRuns: staticData.runCount16 }17}];Expected result: The first execution shows lastRunTime as 'First run'. Subsequent executions show the timestamp from the previous run, proving data persists.
Track the last processed record for incremental syncs
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.
1// Code node: Track last processed ID for incremental sync2const staticData = $getWorkflowStaticData('global');3const lastProcessedId = staticData.lastProcessedId || 0;45// Get all items from the previous node6const items = $input.all();78// Filter to only new items (ID greater than last processed)9const newItems = items.filter(item => item.json.id > lastProcessedId);1011// Update the last processed ID12if (newItems.length > 0) {13 const maxId = Math.max(...newItems.map(item => item.json.id));14 staticData.lastProcessedId = maxId;15}1617return 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.
Use node-scoped static data
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.
1// Node-scoped static data — each node has its own storage2const nodeData = $getWorkflowStaticData('node');34// This 'counter' is independent from any other node's 'counter'5nodeData.counter = (nodeData.counter || 0) + 1;6nodeData.lastSeen = new Date().toISOString();78return [{9 json: {10 nodeCounter: nodeData.counter,11 nodeLastSeen: nodeData.lastSeen12 }13}];Expected result: The counter increments independently for this specific node. Other nodes using the same key name have separate values.
Store complex data structures
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.
1// Store complex data structures in static data2const staticData = $getWorkflowStaticData('global');34// Initialize a deduplication cache5if (!staticData.processedEmails) {6 staticData.processedEmails = {};7}89// Check and update the cache10const items = $input.all();11const newItems = [];1213for (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: 119 };20 newItems.push(item);21 } else {22 staticData.processedEmails[email].count += 1;23 }24}2526// 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}3233return 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.
Use database nodes for larger storage needs
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.
1// PostgreSQL node configuration for storing workflow state2// Operation: Execute Query3// Query:45-- 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);1213-- Upsert state data14INSERT 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();1819-- Read state data20SELECT 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
1// Code node: Complete incremental sync with static data2// Place this after a node that fetches all records (e.g., HTTP Request)34const staticData = $getWorkflowStaticData('global');56// Initialize state on first run7if (!staticData.lastProcessedId) {8 staticData.lastProcessedId = 0;9}10if (!staticData.totalProcessed) {11 staticData.totalProcessed = 0;12}13if (!staticData.lastRunTimestamp) {14 staticData.lastRunTimestamp = null;15}1617const previousLastId = staticData.lastProcessedId;18const items = $input.all();1920// Filter to only new items since last run21const newItems = items.filter(item => {22 return item.json.id > previousLastId;23});2425// Sort by ID to ensure we process in order26newItems.sort((a, b) => a.json.id - b.json.id);2728// Update tracking state29if (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();3536// Add metadata to each item37const enrichedItems = newItems.map(item => ({38 json: {39 ...item.json,40 _syncMetadata: {41 processedAt: staticData.lastRunTimestamp,42 batchSize: newItems.length,43 totalProcessed: staticData.totalProcessed44 }45 }46}));4748// Return new items or a summary if none49if (enrichedItems.length > 0) {50 return enrichedItems;51}5253return [{54 json: {55 message: 'No new items since last run',56 lastProcessedId: staticData.lastProcessedId,57 lastRunTimestamp: staticData.lastRunTimestamp,58 totalProcessed: staticData.totalProcessed59 }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.
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?
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation