FlowKit

getWorkflowStaticData in n8n: persisting state between two executions

Published 1 August 2026 · 6 min read

An n8n workflow is amnesiac by default: every execution starts from scratch, with no memory of the previous one. That becomes a problem the moment a workflow needs to know "where it left off" — last RSS item processed, last ID fetched from an API, a cached token, a list of items already seen. The $getWorkflowStaticData() function exists for exactly this: a small persistent storage space, attached to the workflow, that survives from one execution to the next. This guide covers the syntax in the Code node, the legitimate use cases, the trap that costs almost everyone a few hours (test executions save nothing), and the alternatives once the mechanism hits its limits.

What static data is for

Static data is designed to hold a lightweight working state between two runs of the same workflow:

  • Last-run cursor: the timestamp of the previous pass, so you only fetch items that appeared since ("give me everything newer than X").
  • Last processed ID: the highest ID already seen during incremental polling of an API, a database or a feed.
  • Lightweight cache: an access token obtained from an auth call, reused until it expires instead of being requested on every run.
  • Simple deduplication: a short list of already-processed keys, used to skip repeats — complementing the approaches in our guide to webhook idempotency and duplicate prevention.

This need to remember a resume point between executions is anything but anecdotal: it is a core problem of every data processing system. A study by Paris Carbone, Stephan Ewen and their co-authors, "State Management in Apache Flink: Consistent Stateful Distributed Stream Processing", published in 2017 in the Proceedings of the VLDB Endowment, shows that managing persistent, consistent state (snapshots, cursors, resume points) is the cornerstone of modern stream processing engines — precisely because stateless processing can be neither incremental nor reliable after a restart. n8n's static data is the miniature version of that mechanism: a minimal state, saved at the end of each successful execution.

Syntax in the Code node: 'global' vs 'node'

Static data is manipulated in a Code node (JavaScript), with the same habits covered in our guide to expressions and JavaScript in the Code node:

// Space shared by every node in the workflow
const staticData = $getWorkflowStaticData('global');

// Read (with a default value on the first run)
const lastRun = staticData.lastRun ?? 0;

// Write: just mutate the object — n8n saves it
// automatically when the execution finishes
staticData.lastRun = Date.now();

return $input.all();

Two scopes are available:

  • $getWorkflowStaticData('global'): one object shared by every node in the workflow. Simple, but two nodes writing the same key will overwrite each other.
  • $getWorkflowStaticData('node'): a space private to the current node. Two Code nodes can each keep their own lastId key without conflict — the right choice as soon as a workflow tracks several independent cursors.

One important detail: there is no "save" function to call. You mutate the returned object, and n8n persists its contents when the execution ends. Elegant — and also the source of the following trap.

Trap #1: nothing is saved during test executions

This is behind almost every "getWorkflowStaticData doesn't work" report: static data is not persisted during manual executions launched with the editor's execute button. During a test you can read the existing static data and modify it in memory, but at the end of the run, nothing is written to the database. Only production executions — fired by a webhook, a Schedule Trigger, or an app trigger on an activated workflow — save the state.

Practical consequences:

  1. To test cursor logic, activate the workflow and trigger it for real (call the production webhook URL, or wait for the next Schedule Trigger tick), then inspect the run in the executions list.
  2. Always provide a default value (?? 0, ?? null): on the very first production run, the object is empty.
  3. Never conclude that a cursor "doesn't increment" based on a manual test: that is the expected behaviour, not a bug.

The other traps to know about

Importing, exporting and duplicating the workflow

Static data is attached to the workflow record in your instance's database, not to the JSON file you handle. When you duplicate a workflow, or export it and re-import it elsewhere, the copy starts with an empty state: your cursor resets to zero and the workflow will reprocess the entire history on its next trigger. If you migrate an instance or version your workflows, keep this in mind — our guide to importing and exporting n8n workflows details what travels in the JSON and what stays in the database.

Queue mode and concurrent executions

In queue mode with Redis and multiple workers, each execution loads static data at startup and saves it at the end. If two executions of the same workflow run in parallel, the last one to finish overwrites the other's writes: there is no locking and no merging. For an hourly Schedule Trigger, no risk; for a webhook receiving bursts of simultaneous events, static data is the wrong tool for counting or deduplication — move to external, transactional storage.

Size: a cursor, not a warehouse

Static data is stored in the database together with the workflow, read and rewritten on every execution. It is designed for a handful of lightweight keys — a timestamp, an ID, a small object. Piling up thousands of entries (an ever-growing list of IDs, full API responses) slows down every run and bloats the database. If your deduplication list grows without bound, truncate it (keep only the last N keys) or switch tools.

Concrete example: duplicate-free RSS monitoring

The textbook case, in the spirit of our guide to automated RSS monitoring with n8n: a Schedule Trigger reads a feed every hour, and only genuinely new articles should be sent to Slack or Notion.

const staticData = $getWorkflowStaticData('node');
const lastSeen = staticData.lastSeen ?? 0;

const newItems = $input.all().filter((item) => {
  const published = new Date(item.json.pubDate).getTime();
  return published > lastSeen;
});

if (newItems.length > 0) {
  staticData.lastSeen = Math.max(
    ...newItems.map((i) => new Date(i.json.pubDate).getTime())
  );
}

return newItems;

On the first production run, everything goes through (cursor at 0); afterwards, only articles published since the last pass are forwarded. The same pattern applies to incremental API polling: store the highest id or the most recent updated_at value, then query the API with something like ?since={{ cursor }} on the next run — previously seen items are never even downloaded.

Worth noting: for pure deduplication, the Remove Duplicates node offers an option that remembers items seen in previous executions, without writing a line of code — our Remove Duplicates node guide compares both approaches.

When static data is no longer enough

Three signals tell you it is time to move to real storage:

  • Volume: more than a few dozen keys, or lists that grow on every execution.
  • Concurrency: several simultaneous executions that must read and write the same state without losing writes.
  • Sharing: state consulted by several workflows, or that must survive an instance migration.

The alternatives, in order of simplicity:

  1. n8n Data Tables: native tables, managed in the UI, readable and writable by any workflow — the first move once you outgrow a simple cursor. See our n8n Data Tables guide.
  2. Redis: keys with automatic expiry (perfect for a token cache), atomic counters, locks — ideal for deduplication under heavy concurrency.
  3. A Supabase or PostgreSQL table: durable, queryable state, shared across workflows and instances, with history as a bonus — our guide to connecting n8n to Supabase covers the setup.

Key takeaways

$getWorkflowStaticData('global') (shared) or 'node' (per-node) gives you persistent state between executions, saved automatically at the end of each run — but only in production executions, never during a manual test: that is trap number one. The state lives in the database with the workflow, disappears on duplication or re-import, does not handle queue-mode concurrency, and must stay lightweight: a timestamp, an ID, a few keys. For everything else — volume, concurrent writes, shared state — switch to Data Tables, Redis or a Postgres table. Used properly, it is n8n's simplest tool for turning an amnesiac workflow into a reliable incremental process.

FAQ

Frequently asked questions

Why does my n8n static data stay empty when I test the workflow?

Because static data is not saved during manual test executions (the Execute workflow button in the editor). It is only persisted at the end of a production execution, fired by a real trigger: webhook, Schedule Trigger, or app event. To verify your logic, activate the workflow, trigger it for real, then inspect the run in the executions list.

Where does n8n store getWorkflowStaticData values?

In the n8n instance database, alongside the workflow record itself (SQLite or PostgreSQL depending on your setup). That is why it should only hold lightweight values — a timestamp, an ID, a few keys — rather than large payloads, which would be read and rewritten on every single execution.

How do I reset a workflow's static data in n8n?

From a Code node, delete the relevant keys (delete staticData.myKey) or reassign them, then let the workflow finish via a production execution — a manual run will not save the reset. The radical option: duplicate the workflow, since the copy starts with empty static data.

What is the difference between $getWorkflowStaticData('global') and 'node'?

With 'global', every node in the workflow reads and writes the same shared object. With 'node', each node gets its own isolated space: two Code nodes can both use a lastId key without clashing. Prefer 'node' as soon as a workflow manages several independent counters or cursors.

Bundle FlowKit Complet

€269