I ran the same fix twice and doubled a call-to-action on thirty posts. The script worked perfectly the first time; the trouble was that it worked just as perfectly the second time, stacking a duplicate block on top of the first across a Korea-travel affiliate site I run. That is exactly the failure idempotent scripts exist to prevent: an edit you can run once, twice, or ten times and always land on the same result. Here is how I write WordPress edit scripts that are safe to re-run, so a stray double-execution does nothing instead of quietly corrupting a whole batch.
The day a re-run doubled a block on thirty posts
The first run had failed partway through — a network blip near post twenty — so I did the natural thing and ran it again. What I did not think about was that the first run had already succeeded on the posts it reached before the blip. Running the whole batch again did not resume from where it stopped; it re-applied the insert to every post, including the ones that already had it. The posts that had been edited once now carried the block twice, and I did not notice until a reader-facing page showed two identical calls-to-action stacked on each other.
Cleaning it up was slower than the original job, because now I had to detect and remove one of two identical blocks without touching posts that only had one. The real lesson was not “be careful re-running scripts.” Re-running is going to happen — a timeout, a rate limit, an interrupted run, a colleague who is not sure if it finished. A batch script has to assume it will be run more than once and behave correctly when it is. That property has a name, and building for it changed how I write every edit since.

What idempotent scripts actually guarantee
Idempotent scripts guarantee one thing: the state after running is the same no matter how many times you run. Apply the fix to a post that already has it and nothing changes. Apply it to a post that does not and the fix appears exactly once. The script stops being a blind “do this action” and becomes a “make sure this is true” operation — it moves the post toward a desired end state rather than performing a step. That reframing is the whole idea, and every technique below is just a way to enforce it.
The payoff is that re-running becomes boring instead of dangerous. A failed batch can be re-run in full without a second thought, because the posts that already succeeded are simply skipped. There is no need to track which posts were done, no fragile “resume from index 20” logic, no anxiety about double execution. This is one of the properties that lets automation run inside the content pipeline I run without a human babysitting every batch: a re-run is a no-op, by construction.
The existence-marker guard
The simplest guard is a marker. When my script inserts a block, it wraps that block in an HTML comment the script can recognize later, something like a named boundary that means “this edit already ran here.” Before inserting on any post, the script searches the fetched content for that marker. If the marker is present, the post is already done and the script skips it entirely. If it is absent, the script inserts the block, marker included, so the next run will recognize it.
The marker has to be part of what you write, not something you track in a separate file. State kept outside the content drifts out of sync the moment someone edits a post by hand or you run from a different machine. A marker embedded in the post’s own stored content travels with the post and is always accurate, because it is the post. It also doubles as documentation: months later, that comment tells me which script touched this post and why the block is there.
The count guard for the anchor
The marker prevents re-insertion, but I add a second guard for the insertion point itself. Before writing, the script confirms the anchor it plans to insert against appears in the content exactly once — a simple count of the target string equalling one. If the anchor appears zero times, the post does not match the assumption the script was written under, and I would rather skip and log it than guess. If it appears more than once, inserting could land in the wrong place, so the script refuses and flags the post for a human.
This “exactly one” check has caught more surprises than the marker ever did. Posts are not uniform; a template you assumed was everywhere is missing on the oldest articles and duplicated on a few odd ones. The count guard turns those mismatches into a clean skip-and-report instead of a silent wrong edit. The log it produces is useful on its own, too: the list of skipped posts is a map of exactly where your assumptions about the archive are wrong, which is often worth fixing before you touch anything else. The Posts endpoint reference is worth reading alongside this, because knowing exactly which field you fetch and write is half of getting the count right.

Invariant checks after every write
Idempotence keeps a re-run from adding duplicates, but it does not by itself prove the edit was correct. So the last layer is an invariant check on the new content before it is pushed. The rules are simple and post-agnostic: every application/ld+json block must still parse, the heading count must match what I fetched unless I meant to change it, and every table must still carry its responsive wrapper. If any invariant fails, the write is refused and the post is logged, not pushed.
Parsing the schema is the check I never skip, because a broken structured-data block is invisible on the page and expensive in Search Console. Google’s structured data documentation explains what valid markup has to look like, and a one-line parse attempt in the script enforces it before anything ships. Together the guards form a small contract: skip if already done, refuse if the shape is wrong, write only when both are satisfied. Run that script as many times as you like and the archive only ever moves toward correct.
| Naive script | Idempotent script | |
|---|---|---|
| Run once | Inserts the block | Inserts the block |
| Run twice | Inserts it again (duplicate) | Sees the marker, skips |
| Anchor missing | Errors or edits wrong spot | Count guard skips and logs |
| After an interrupted run | Unsafe to re-run | Re-run the whole batch freely |
| Bad output | Ships silently | Invariant check refuses the write |
FAQ
What does it mean for a WordPress edit script to be idempotent?
It means running the script more than once produces the same result as running it once. Applied to a post that already has the change, it does nothing; applied to one that does not, it makes the change exactly once. The script becomes a make-sure-this-is-true operation rather than a blind action.
How do I stop a script from duplicating a block on re-run?
Wrap the inserted block in a marker, such as an HTML comment, and check for that marker before inserting. If it is present the post is already done and you skip it. Keep the marker inside the post’s stored content, not in a separate file, so it stays accurate across machines and hand edits.
Why check that the anchor appears exactly once?
Because posts are not uniform. An anchor you expected everywhere may be missing on old posts or duplicated on a few. Counting it and requiring exactly one turns those mismatches into a clean skip-and-report instead of a silent insertion in the wrong place. Zero or more than one means skip and log for a human.
Are idempotent scripts enough to guarantee a correct edit?
No. Idempotence prevents duplicates on re-run but does not prove the edit is right. Add an invariant check before writing: confirm every JSON-LD block still parses and the heading and table counts match what you fetched. If an invariant fails, refuse the write and log the post rather than pushing broken content.
Can I safely re-run a batch that failed partway through?
If the script is idempotent, yes. The posts that already succeeded carry the marker and are skipped, and the ones that did not get the change exactly once. There is no need to track which posts were done or resume from an index, because a re-run of the full batch is a no-op on the finished posts.
My Thoughts
What I took from the doubled-CTA mess was a shift in what a script is for. I used to write scripts that performed a step; now I write scripts that assert a state and make it true. The difference sounds academic until a run gets interrupted at post twenty and you have to decide, at speed, whether re-running will help or hurt. With the old style that is a genuinely scary decision. With the new one it is not a decision at all — you just run it again. Designing for the second run, not the first, is the small mindset change that took editing at scale from nerve-wracking to routine.
