Moving 300 posts to a different category, publishing every draft matching a condition, or cleaning up a tag takes an afternoon in the admin. On the command line it is two lines. Why it has to be two lines is the point of this part.
List before you change
wp post list accepts nearly all of WP_Query‘s arguments as flags, so most filters you can express in the admin list table work here too.
wp post list --post_type=post --post_status=draft --fields=ID,post_title,post_name
wp post list --post_type=post --category_name=seo --format=csv
wp post list --post_type=post --posts_per_page=-1 --format=ids
--fields picks the columns, --format chooses between a table, CSV, JSON or a bare list of IDs. That last one, --format=ids, is the raw material for the next step.
The rule is simple. Keep the command that selects and the command that changes separate, and run the selecting one on its own first. Only once the printed list matches the list you intended do you wrap it in something that writes.
Creating — –porcelain makes it composable
Add --porcelain when creating and you get just the new ID instead of a friendly success message, which means it feeds straight into the next command.
ID=$(wp post create --post_type=page --post_title='Pricing' --post_name=pricing --post_status=draft --porcelain)
wp post meta update $ID _wper_layout wide
wp post term set $ID category performance
For anything longer than a sentence, do not cram the body into a flag — read it from a file. wp post create ./body.html --post_title='Pricing' takes the first positional argument as the content file, which is the easiest escape from shell quoting purgatory.
Terms and meta
Terms follow the same shape: see what exists, then attach.
wp term list category --fields=term_id,slug,name,count
wp term create wper_role developer --slug=developer
wp post term add 123 wper_level practical
wp post meta list 123
term set and term add are not interchangeable. set replaces every existing term in that taxonomy; add appends. Use set where the rule is one term per post, and add where terms accumulate. Swap them by accident and your taxonomy quietly flattens.
If a meta value needs to be an array or an object, use --format=json. Never hand-write a serialised string — the next part is about exactly that serialisation coming apart.
Bulk edits without casualties
Once the selection is confirmed, the write is short. Put the list command inside it verbatim.
wp post update $(wp post list --post_type=post --post_status=draft --category_name=seo --format=ids) --post_status=publish
A few habits keep this safe.
wp post delete moves items to the trash by default and deletes permanently with --force. Permanent is permanent. Getting into the habit of running wp db export before any mass deletion costs seconds, and there is no substitute for it on the day you need it.
How to structure content so this kind of work stays easy is covered in the development workflow archive, and if you would rather hand the restructuring and upgrade over as one job, see our optimization program.
Next part
Next up: changing a domain. It looks like a one-line SQL job, and that is precisely where settings quietly disappear. The reason is serialisation.