Sharing a finding that cost me a few hours, in case it saves someone else the same afternoon.
The pattern. A very common way to make a scheduled workflow idempotent is to keep a list of already-processed ids in workflow static data:
mark id as done → do the work (send message / call API / write row) → done
```
**The problem.** n8n persists static data on the `workflowExecuteAfter` hook, and that hook fires whether the execution **succeeded or failed**. I checked it in the source and then reproduced it: a workflow that marks an id done and *then* throws still has the "done" write persisted.
So when the work step fails:
- the id is marked done
- the work never happened
- the execution shows as failed, so you assume you can just re-run it
- on the re-run the id is already "done" → the item is skipped, permanently
Nothing surfaces it. The execution list looks healthy and the "processed" count even looks right — which is what makes it nasty: the failure mode is *silence*, and it triggers exactly when something downstream breaks, which is the situation you added dedup for in the first place.
**The fix is ordering, not code.** Commit after the side effect succeeds, per item:
```
check if done → do the work → only now mark it done
```
In a Code node it's just moving the write to the end:
```js
// runs only AFTER the side effect succeeded, once per item
const state = $getWorkflowStaticData('global');
state.done = state.done || [];
const key = String($json.id);
if (!state.done.includes(key)) state.done.push(key);
return $input.item;
```
Two things worth knowing when you do this:
1. **Per item, not per batch.** If you mark a whole batch done at the end, one failure re-does the rest on the next run (and re-sends those messages). Commit each item right after its own side effect.
2. **This is at-least-once, not exactly-once.** If the process dies between the side effect and the commit, that one item repeats on the next run. For most work that's the right trade — a visible duplicate beats a silent loss — but it's a choice, so make it deliberately.
If you have a scheduled workflow with a dedup list, it's worth opening it and checking the order of those two steps. Thirty seconds, and it's either fine or it's been quietly dropping things.