You need to change 400 posts. Move them to draft, reassign an author, add a category, fix a URL that appears in all of them. In the admin that is an afternoon of pagination and checkboxes, and one misclick you will not notice.
The short answer: select the posts with wp post list --format=ids, pass those IDs to a command that accepts several at once, such as wp post update, wp post delete or wp post term add, and change text across the database with wp search-replace after a --dry-run. Export the database with wp db export before any of it. WP-CLI does in one line what the admin does in hundreds of clicks, and it undoes nothing for you, which is why the backup comes first.
Commands below are quoted from the WP-CLI command reference on developer.wordpress.org, read 2026-09-11. The latest WP-CLI release on the project’s GitHub releases page that day was 2.12.0. The examples use POSIX shell syntax, the $( ... ) substitution in particular, as the documentation does, so run them in a Bash-style shell on the server.
Step zero: back up the database
Every command in this article that changes data acts immediately on the live database, with no undo. Before you run one against a production site, export a backup. From the wp db export reference:
# Export database with drop query included
$ wp db export --add-drop-table
Success: Exported to 'wordpress_dbase-db72bb5.sql'.
For a change that only touches a handful of posts, the same page shows exporting just those rows, which makes a targeted restore much simpler:
# Export certain posts without create table statements
$ wp db export --no-create-info=true --tables=wp_posts --where="ID in (100,101,102)"
Success: Exported to 'wordpress_dbase-db72bb5.sql'.
Move the file out of the web root. A database dump in a public folder is a data leak.
The pattern: select, count, then act
Every bulk operation has the same shape, and the first two steps are the ones people skip.
1. Select. wp post list accepts WP_Query arguments as --<field>=<value> and can print only IDs:
$ wp post list --post_type=page,post --format=ids
2. Count and inspect. Run the same selection with --format=count, then look at a sample with readable fields:
$ wp post list --post_type=page --fields=post_title,post_status
If the count is not the number you expected, your selection is wrong, and the action step would have been wrong too. Everything in debugging code that runs but gives the wrong result applies here: a selector that silently matches too much is not an error, it is a bad result that looks like success.
3. Act, feeding the same selection in.
Updating many posts at once
wp post update takes one or more IDs. The reference’s examples:
$ wp post update 123 --post_name=something --post_status=draft
Success: Updated post 123.
$ wp post update 123 --meta_input='{"key1":"value1","key2":"value2"}'
Success: Updated post 123.
$ wp post update 123 456 --post_author=789
Success: Updated post 123.
Success: Updated post 456.
Combining that with the selection step, reassigning every post by author 2 to author 789 looks like this:
wp post update $(wp post list --author=2 --format=ids) --post_author=789
Two options on that page are worth knowing for bulk work:
--defer-term-counting, described as “Recalculate term count in batch, for a performance boost.” Without it, term counts are recalculated after each post.--post_status. Changing a published post todrafttakes it off the live site immediately. On a client’s site, that is a decision for the site owner, not a side effect of a cleanup.
Meta and terms
For a single meta key per post, wp post meta update takes <id> <key> [<value>], with the value read from STDIN if omitted and --format=json for structured values. It works on one post at a time, so for many posts you either loop in the shell or use --meta_input on wp post update, which accepts several IDs.
To add a category or tag to posts, wp post term add takes <id> <taxonomy> <term>... and “appends the term to the existing set of terms on the object,” so existing terms stay. Terms are matched by slug unless you pass --by=id.
Deleting in bulk: trash first
wp post delete accepts many IDs, and the reference shows the selection pattern directly:
# Delete all pages
$ wp post delete $(wp post list --post_type='page' --format=ids)
Success: Trashed post 1164.
Success: Trashed post 1186.
Note the output says Trashed. Without --force, posts go to the trash and can be restored from the admin. With --force (“Skip the trash bin”) they are gone. Run the delete without --force, check the site, and empty the trash later, once you are sure:
# Delete all posts in the trash
$ wp post delete $(wp post list --post_status=trash --format=ids)
Success: Deleted post 1268.
Success: Deleted post 1294.
Changing text everywhere: search-replace
When the change is text inside content, such as a domain after a migration or an old shortcode, wp search-replace is the tool. The reference notes that it “intelligently handles PHP serialized data, and does not change primary key values.” That is the reason to use it instead of a raw SQL UPDATE ... REPLACE(), which corrupts serialized values whose string lengths change.
Always run it with --dry-run first. The reference defines that flag as running “the entire search/replace operation” and showing the report without saving. Its example:
wp search-replace 'foo' 'bar' wp_posts wp_postmeta wp_terms --dry-run
Read the report table: which tables, which columns, how many replacements. Then remove --dry-run and run it for real. Other options from the same page:
--skip-columns=guidleaves theguidcolumn alone; the reference’s domain example uses it.--preciseforces PHP instead of SQL, “more thorough, but slower.”--export=<file>writes the transformed data to an SQL file instead of changing the database, which gives you a reviewable artifact.
Multisite, plugins and large sites
Multisite. WP-CLI’s global parameters (read 2026-09-11) include --url=<url>, which “In multisite… is how the target site is specified.” Without it, commands act on the main site. If you converted a site into a network, the URL structure behind that is covered in converting a single site to a directory-based multisite.
A plugin fatal stops WP-CLI. WP-CLI loads WordPress, so a broken plugin breaks the command too. --skip-plugins loads WordPress without them; the reference notes mu-plugins still load. --skip-themes does the same for themes. Be careful: a bulk update run with plugins skipped also skips any plugin hooks that would normally fire on save. If the error is not obvious from the terminal, reading a WordPress debug log shows how to name the code responsible.
Large sites. Very long ID lists passed through $( ... ) can exceed the shell’s argument length limit, and loading thousands of full post objects in one PHP process can exhaust memory. If you see the memory error, the Allowed memory size exhausted fix explains why batching beats raising the limit. Split the selection into batches with --posts_per_page and --paged, which are WP_Query arguments wp post list passes through.
When the change is logic, not a field
Some bulk jobs are not “set field X to Y” but “compute something for each post.” wp eval-file (reference read 2026-09-11) loads and executes a PHP file with WordPress loaded, and passes extra positional arguments to it as $args. The reference notes that code runs inside a method, so globals must be declared with global. It is fine for a one-off. For anything you will run twice, a registered command with its own --dry-run flag and confirmation prompt is safer and documents itself.
A pre-flight checklist
- Database exported, file stored outside the web root.
- Selection run with
--format=countand a sample inspected. - Correct site targeted (
--urlon multisite). - Destructive flags removed: no
--forceon the first pass,--dry-runon every search-replace. - Change run, then verified on the front end and in the admin.
- Trash emptied and backup deleted from the server only after the site owner confirms the result.
FAQ
How do I bulk update posts with WP-CLI?
Get the IDs with wp post list --format=ids and your filters, then pass them to wp post update with the field to change, for example --post_author=789. wp post update accepts several IDs at once. Export the database with wp db export first.
Is wp post delete permanent?
Not by default. Without --force, the WP-CLI reference shows posts being trashed, which you can restore. With --force the trash is skipped and the posts are deleted.
Is wp search-replace safe for serialized data?
The WP-CLI reference states that it handles PHP serialized data and does not change primary key values. Still run it with --dry-run first and review the report before running it for real.
How do I target one site in a multisite network?
Add the global --url=<url> parameter. The WP-CLI reference describes it as how the target site is specified in multisite.
