wp te-batch update_meta --meta_key=currency --value=USDA bulk update should preview changes before writing, verify a private backup before the first mutation, and record every attempted change. The downloadable Safe Batch plugin implements that workflow for one native text-meta key on at most 5,000 posts, with a separate restore command.
This is a bounded example for PHP 8.1+ and WP-CLI. It rejects duplicate values, arrays, objects and oversized values rather than claim it can restore arbitrary WordPress or ACF data. Pause editors, cron jobs and other importers during a live run. The tool's file lock coordinates its own runs, not every writer on the site.
Correction: the earlier snippet did not check backup writes, placed the backup inside wp-content, and only displayed the first 20 changes. Version 1.1 checks the snapshot and journal before writing, uses private files outside WordPress, and records each attempt and result.
The command
Download te-safe-batch.php, inspect it, place it in wp-content/plugins/te-safe-batch.php, then activate it:
wp plugin activate te-safe-batch
wp help te-batch update_meta
wp help te-batch restoreThe downloadable file is the complete plugin. It registers wp te-batch update_meta and wp te-batch restore; the underscore in update_meta is part of the command name. Its te-/TE_ identifiers avoid collisions with other plugins.
Run the preview first. It prints one JSON record per proposed change, including the post ID and previous/new value lists:
wp te-batch update_meta --meta_key=currency --value=USD --post_type=postAn empty list means the key was absent. A list containing an empty string means the key existed with an empty value. That distinction matters when restoring. Preview output can contain the selected field's values; do not capture sensitive fields in a shared terminal log.
The query covers the selected post type and WordPress's any post-status set, which does not mean literally every possible status. It refuses a result over 5,000 posts. The example holds its bounded plan in memory; it does not promise constant memory or unbounded scale. For a larger job, partition the targets and use the WP-CLI bulk-script guide to design explicit checkpoints.
The four rails
| Check | What the plugin actually does |
|---|---|
| Dry run | Reads the proposed changes and prints them; creates no backup, journal or meta writes |
| Backup gate | Creates an exclusive private snapshot, checks full writes and synchronization, then reads it back byte-for-byte |
| Journal gate | Opens a private journal and synchronizes its start record before the first database write |
| Checked writes | Rechecks the old value, records the attempt, writes, verifies the stored value and records the result |
The snapshot contains the site URL, blog ID, table prefix, post type, key and every planned before/after value. That provides context for restoration, not a substitute for a full database backup. Keep a tested full backup for complex or consequential operations.
The critical write sequence is:
// Abbreviated from the downloadable plugin; not a second standalone plugin.
$this->record( $journal, array( 'event' => 'attempt', 'entry' => $entry ) );
$result = $entry['after']
? update_post_meta( $id, wp_slash( $key ), wp_slash( $entry['after'][0] ) )
: delete_post_meta( $id, wp_slash( $key ) );
if ( false === $result || $this->values( $id, $key ) !== $entry['after'] ) {
$this->record( $journal, array( 'event' => 'failed', 'id' => $id ) );
throw new RuntimeException( "Post {$id} failed verification; earlier posts may have changed." );
}
$this->record( $journal, array( 'event' => 'applied', 'id' => $id ) );update_post_meta() returning false can mean an unchanged value or a failure. The plan already excludes no-ops, and each write is followed by a value check. wp_slash() preserves literal backslashes and quotes in both keys and values across WordPress's metadata write handling. Before reading a field, the plugin rejects stored keys that compare equal under the database collation but have a different spelling, such as Currency and currency. That avoids changing a field missing from the snapshot.
A dry run, then the real thing
Create the backup directory as the same operating-system user that runs WP-CLI. It must sit outside ABSPATH and every other served directory or web-server alias:
install -d -m 700 "$HOME/te-batch-backups"
wp te-batch update_meta --meta_key=currency --value=USD
wp te-batch update_meta --meta_key=currency --value=USD \
--backup-dir="$HOME/te-batch-backups" --liveThe plugin rejects a missing/unwritable directory, a directory inside WordPress, content or uploads (including external paths), or group/world-accessible directory permissions. Files are created exclusively with restrictive permissions and random names. It cannot discover every web-server alias on your host; check the location yourself. A backup containing private values must never be downloadable over HTTP.
A live run prints its actual snapshot and journal paths. Do not copy a placeholder filename from another run. A second identical successful run reports no changes.
Restore from the snapshot
Use the path printed by the original run:
wp te-batch restore --backup=/srv/private/te-batch-SNAPSHOT_ID.json
wp te-batch restore --backup=/srv/private/te-batch-SNAPSHOT_ID.json --liveReplace SNAPSHOT_ID with the real generated name. The first invocation previews restoration; the second applies it after review. Restoration rejects the wrong site context, missing posts, duplicate entries and unsupported value types. It refuses to overwrite a field that no longer matches either the original or intended updated value, so unrelated later edits require a manual decision.
An originally absent key is deleted; an originally empty string is restored as an existing empty value. Successful restoration is repeatable. Keep the source snapshot until you have verified the restored application behavior.
This is not an all-or-nothing database transaction. A failure after earlier writes leaves a partial run. Every write has a preceding journal attempt; if the process or disk fails after the database write but before its result record, that attempt has an uncertain outcome. Inspect the actual value and snapshot instead of assuming a missing applied line means no change happened.
Prove the rails with a test
Use a disposable WordPress database, not the production catalog. Cover these behaviors before adapting the plugin:
- A dry run leaves both values and backup directory unchanged.
- A missing, web-root or non-private backup directory fails before any meta write.
- Duplicate and structured metadata are rejected before any mutation.
- More than 20 changes produce matching attempt/result journal records for every changed post.
- A simulated database failure stops the run, reports an error and preserves the snapshot for partial recovery.
- Restoration distinguishes absent, empty and existing values, preserves literal quotes/backslashes, and rejects later conflicting edits.
- Running the same update or restore twice produces no further changes.
The download supplies the implementation, not a universal safety guarantee. A plugin's metadata filters or external side effects can change behavior, so test with the relevant application plugins enabled too.
ACF fields need a field-specific updater
Do not blindly swap get_post_meta() for get_field() in this plugin. ACF formatted values, field-reference rows, Repeaters, relationships and images can have different storage and restoration requirements. Use field keys and a field-specific snapshot/write/restore procedure; ACF versus native metadata explains the distinction.
For WooCommerce, use product CRUD methods so lookup tables and caches stay consistent. The WooCommerce sheet updater has a different data model and should not use this text-meta command for prices or stock.
If the input comes from a spreadsheet, validate it before building the change plan. The WordPress sheet-sync guide covers that separate input path.
Sources
Authoritative references this article was fact-checked against.
- WP-CLI Commands - WP-CLI Command Referencedeveloper.wordpress.org
- WP_CLI::add_command() - WP-CLI Handbookmake.wordpress.org
- update_post_meta() - WordPress Developer Referencedeveloper.wordpress.org
- wp db export - WP-CLI Command Referencedeveloper.wordpress.org
- WP_CLI\Utils\format_items() - WP-CLI Handbookmake.wordpress.org
- get_post_meta: single versus multiple valuesdeveloper.wordpress.org
- wp_slash: preserving literal metadata stringsdeveloper.wordpress.org
- PHP fsync: synchronizing file contentsphp.net





