← Overview

SBBy · · 4 min read

Bulk updates in Storyblok: 8 stories or 80,000, the same five steps

Ask for a list, approve it or its rule, dry-run, patch in place, read it back. Only the input and the approval change with scale.

TL;DR How I change content across many Storyblok stories with a local agent: a proposals list first, a dry run, in-place patches, and a read-back. The same steps work for 8 stories or 80,000+.

Sooner or later every content team needs the same change in many places: a product gets renamed, a link moves, a field type gets replaced. By hand it is slow. With a quick script it is risky. This week I did a series of these bulk updates on my own blog in Storyblok, with a local agent writing the scripts, and the part that mattered wasn't the script. It was how the work was asked for and approved.

The idea I took away: whether it's 8 stories, 800 or 80,000+, the steps stay the same. Only the input and the approval change.

What I changed this week

All on draft stories in one Storyblok space, all scripted against the Management API, nothing published:

  • 13 links to a placeholder repository, replaced with the real URL across eight articles, and later re-cased in all 13 places.
  • 9 code blocks moved to a new code editor field: 8 code components in one article, plus one code block that lived inside rich text.
  • 15 cross-links added between articles, on phrases that were already in the text. No wording changed.
  • An editorial pass: 36 proposed edits for tone and length. I rejected 4, rewrote 3 by hand because they crossed links or bold text, and applied 34.

1. Ask for a list, not a change

The first request is never "update all stories". It's "show me what you would change". For the editorial pass, the agent wrote a proposals file, one entry per edit, with the exact current text and the replacement:

edit-proposals.jsonjson
{
  "slug": "prompt-contract-ai-generated-storyblok-field-plugins",
  "address": "[35:4]",
  "kind": "tone",
  "find": "Option values are not deployed.",
  "replace": "Option values are set per field.",
  "reason": "Neutral framing; the next sentence keeps the exact fact (deploy ships names only)."
}

Exact text matters. It is what lets a script find the right spot later, and what lets a person judge the change without opening the story.

2. Approve the list, or the rule

With 36 proposals I could read every one, and it paid off. One proposed cut would have removed a sentence I had asked to add a few minutes earlier. The agent proposing it didn't know that. Another would have left the next sentence without its subject.

At 80,000+ stories nobody reads every entry. Then you approve the rule instead: what gets matched, a sample of real before/after pairs, and the dry-run summary. The list still exists. You just stop reading it line by line.

3. Dry run against the live content

Before anything is written, the script runs the whole list against the current drafts and reports per entry: applies cleanly, text not found, or crosses formatting. In my run, 3 of the remaining 32 edits crossed a link or bold text. Those I rewrote by hand as smaller edits that left the formatting alone.

4. Patch in place, don't re-import

Re-importing a story from its source overwrites whatever happened to it in the editor since. So the scripts read each story's current draft, change only the matched text, and save. A few rules made this safe:

  • Check who touched it last. Before overwriting, compare the story's last update with your last write. One of my articles had been edited in the visual editor after my import, and this check left it alone.
  • Never publish from a script. Drafts stay drafts until a person publishes them.
  • Make every run repeatable. A second run should find nothing left to do. My code block migration reported 0 on its second run.
  • Respect rate limits. The scripts back off when the API answers "too many requests", which matters more at 80,000 than at 8.

5. Read it back

"The script said OK" is not the end. Two problems this week only showed up when I checked the result:

  • A prompt box with a Copy button looked fine, but the clipboard came back empty. A comment line in the source had broken the code block during import. Comparing the copied text with the stored prompt caught it.
  • After replacing images in place, the site still showed the old versions at sizes it had requested before, because resized images are cached per URL. New file names fixed it.

Since then, a small audit script reads every article back and reports broken internal links, leftover placeholders and who links to whom.

MCP or scripts?

For a handful of quick, exploratory changes, an agent talking to Storyblok through the MCP server is the fastest way to work. For anything you want to review, repeat or run at scale, I prefer scripts that the agent writes: they produce the proposals file, the dry-run report and the read-back, and you can run them again tomorrow.

It reminds me of database migrations in Ruby on Rails. Every change is a small, named script in a shared repository, so you can see what changed, when and why, and run the same step again later. Content deserves the same: a shared repository for content migrations, where each bulk update is a script with its proposals file and dry-run report next to it. You don't need to be a great developer to keep one. The agent writes the scripts, and the team reviews the lists.

Storyblok environments usually work great for this as well: run a migration against a staging environment first, check the result there, then run the same script against production.

What changes with scale

  1. Input: at 8 stories you can name them. At 80,000+ it is a query: every story with the old link, every code block in the old format.
  2. Approval: at 8 you approve each item. At 80,000+ you approve the rule, a sample and the dry-run summary.

Everything in between stays the same: list first, dry run, patch in place, never publish from a script, read it back. That is also how I work with agents in general: one coordinator that owns the outcome, and every result checked before it counts.