Skip to main content

load_value, update_value, and format_value Filters

Value filters are reliable only when each one owns a single lifecycle responsibility.

Learning Focus

You will separate compatibility transforms (load_value), canonical storage normalization (update_value), and presentation-only formatting (format_value), then verify each stage with CLI tests.

Concept Overview

ACF value filters exist at three moments: value enters runtime (load_value), value is written (update_value), and value is prepared for output (format_value). Confusing these stages is the fastest way to create hard-to-debug data bugs.

update_value should normalize canonical storage shape: strip noise, enforce prefixes, cast types, and persist stable values. format_value should never mutate canonical storage. It exists for output representation only. load_value is often used for backward compatibility or migration bridges where old storage formats must be interpreted safely.

A clean strategy means each callback is pure for its stage: predictable input, predictable output, and no unexpected side effects. This improves queryability, keeps migrations reversible, and avoids hidden mutations during rendering.

Core Idea

Store canonical values with update_value, adapt legacy reads with load_value, and shape display output with format_value only.

Why It Matters

ApproachWhat HappensImpact in Production
Normalize in update_value onlyStored values stay queryable and consistentCleaner analytics, safer migrations
Format in format_value onlyPresentation concerns stay in output layerLower risk of data corruption
Use load_value for compatibility bridgesLegacy records remain readable during transitionsSmoother phased migrations
Keep callbacks idempotent and scopedRe-runs and retries remain safeMore reliable release operations
Mix normalization and formatting randomly (wrong pattern)Storage shape drifts and output becomes inconsistentExpensive cleanup and hidden regressions

Reference Table

Term/APISignature/SyntaxPurposeKey Notes
acf/load_valueadd_filter('acf/load_value', callable $callback, int $priority = 10, int $accepted_args = 3): voidTransform values when loading from DBUse for compatibility, not persistence
acf/update_valueadd_filter('acf/update_value', callable $callback, int $priority = 10, int $accepted_args = 4): voidNormalize values before writeBest place for canonical storage rules
acf/format_valueadd_filter('acf/format_value', callable $callback, int $priority = 10, int $accepted_args = 3): voidFormat values for output/read consumptionDo not write DB changes here
acf/validate_valueadd_filter('acf/validate_value', callable $callback, int $priority = 10, int $accepted_args = 4): voidReject invalid values before update stagePair with update_value for robust pipelines
update_field()`update_field(string $selector, mixed $value, intstring $post_id = false): intbool`
wp post meta getwp post meta get <post-id> <meta-key>Inspect raw stored canonical valueConfirms storage shape after update filter
wp evalwp eval '<php-code>'Test filter outputs directlySupports stage-by-stage assertions
Repeater row filtersacf/update_value/name=service_faq_itemsNormalize structured arraysACF Pro Required when target is Repeater/Flexible

Hook Targeting Syntax

Prefer targeted filter variants for precision:

VariantExampleUse
Globalacf/update_valueBroad diagnostics only
Name-targetedacf/update_value/name=sales_phoneStable field names with ownership policy
Key-targetedacf/update_value/key=field_sales_phoneHigh-precision production rules
Type-targetedacf/format_value/type=numberShared formatting policy by type

Practical Use Cases

Use Case 1 — Normalize phone numbers before save and keep storage canonical

A sales form receives phone numbers in inconsistent formats (+65 8123-9999, 081239999, (65) 8123 9999). You want one canonical format in storage: +65XXXXXXXX.

  1. Validate minimum digit quality in validate_value.
  2. Normalize storage in update_value.
  3. Keep output formatting separate from storage rules.
  4. Verify raw stored value with wp post meta get.
  5. Assert filter output through CLI.
wp-content/themes/clinic-pro/inc/acf/phone-normalization.php
<?php

declare(strict_types=1);

add_filter('acf/validate_value/name=sales_phone', function ($valid, $value, $field, $input) {
if ($valid !== true) {
return $valid;
}

$digits = preg_replace('/\D+/', '', (string) $value);
if (strlen((string) $digits) < 8) {
return 'Phone number must contain at least 8 digits.';
}

return true;
}, 10, 4);

add_filter('acf/update_value/name=sales_phone', function ($value, $post_id, $field, $original) {
$digits = preg_replace('/\D+/', '', (string) $value);

if (str_starts_with((string) $digits, '0')) {
$digits = substr((string) $digits, 1);
}

if (!str_starts_with((string) $digits, '65')) {
$digits = '65' . $digits;
}

$normalized = '+' . $digits;
error_log('[acf-phone-update] post=' . (string) $post_id . ' value=' . $normalized);

return $normalized;
}, 20, 4);

add_filter('acf/format_value/name=sales_phone', function ($value, $post_id, $field) {
$digits = preg_replace('/\D+/', '', (string) $value);
if (strlen((string) $digits) < 10) {
return (string) $value;
}

return '+' . substr((string) $digits, 0, 2) . ' ' . substr((string) $digits, 2, 4) . ' ' . substr((string) $digits, 6);
}, 10, 3);
terminal: command
wp eval '$v=apply_filters("acf/update_value/name=sales_phone","(65) 8123-9999",1360,["name"=>"sales_phone"],"(65) 8123-9999"); var_export($v); echo PHP_EOL;'
wp eval 'update_field("sales_phone","(65) 8123-9999",1360); echo get_field("sales_phone",1360,false) . PHP_EOL;'
wp post meta get 1360 sales_phone
terminal: output
'+6581239999'
+6581239999
+6581239999
warning

If canonical format is not enforced at update stage, every downstream system (CRM sync, analytics, exports) must re-normalize independently.

Use Case 2 — Use load_value as legacy compatibility bridge during migration window

Old records store service status as uppercase words (ACTIVE, PAUSED), while new schema expects lowercase slugs (active, paused). You need temporary compatibility without rewriting all rows immediately.

  1. Keep canonical writes in lowercase through update_value.
  2. Add load_value map for legacy uppercase reads.
  3. Keep bridge logic explicit and auditable.
  4. Add migration report for converted reads.
  5. Plan bridge removal after full migration.
wp-content/themes/clinic-pro/inc/acf/status-compat-bridge.php
<?php

declare(strict_types=1);

add_filter('acf/load_value/name=service_status', function ($value, $post_id, $field) {
$raw = (string) $value;
$map = [
'ACTIVE' => 'active',
'PAUSED' => 'paused',
'ARCHIVED' => 'archived',
];

$resolved = $map[$raw] ?? strtolower($raw);

$report = get_option('acf_status_load_bridge_report', []);
if (!is_array($report)) {
$report = [];
}

if ($raw !== $resolved) {
$report[] = [
'post_id' => (int) $post_id,
'raw' => $raw,
'resolved' => $resolved,
'at' => gmdate('c'),
];
update_option('acf_status_load_bridge_report', array_slice($report, -100), false);
}

return $resolved;
}, 10, 3);

add_filter('acf/update_value/name=service_status', function ($value, $post_id, $field, $original) {
return strtolower(trim((string) $value));
}, 10, 4);

add_filter('acf/format_value/name=service_status', function ($value, $post_id, $field) {
$v = (string) $value;
return $v === '' ? '' : ucfirst($v);
}, 10, 3);
terminal: command
wp eval '$v=apply_filters("acf/load_value/name=service_status","ACTIVE",1360,["name"=>"service_status"]); var_export($v); echo PHP_EOL;'
wp eval 'update_field("service_status","PAUSED",1360); echo get_field("service_status",1360,false) . PHP_EOL;'
wp eval 'echo get_field("service_status",1360,true) . PHP_EOL;'
wp eval 'print_r(get_option("acf_status_load_bridge_report",[]));'
terminal: output
'active'
paused
Paused
Array
(
[0] => Array
(
[post_id] => 1360
[raw] => ACTIVE
[resolved] => active
[at] => 2026-02-23T14:41:07+00:00
)
)
note

Compatibility bridges should be time-bounded. Keep a deprecation date so temporary load logic does not become permanent debt.

Use Case 3 — Edge case: format_value callback mutates storage (anti-pattern)

A callback mistakenly writes back to DB inside format_value, causing side effects during rendering and potential recursion/noise in caches and logs.

❌ Fragile Pattern

wp-content/themes/clinic-pro/inc/acf/format-fragile-mutation.php
<?php

declare(strict_types=1);

add_filter('acf/format_value/name=service_slug', function ($value, $post_id, $field) {
$slug = sanitize_title((string) $value);
update_field('service_slug', $slug, (int) $post_id);
return $slug;
}, 10, 3);

✅ Robust Pattern

wp-content/themes/clinic-pro/inc/acf/format-robust-pure.php
<?php

declare(strict_types=1);

add_filter('acf/update_value/name=service_slug', function ($value, $post_id, $field, $original) {
return sanitize_title((string) $value);
}, 10, 4);

add_filter('acf/format_value/name=service_slug', function ($value, $post_id, $field) {
$slug = sanitize_title((string) $value);
$display = str_replace('-', ' ', $slug);
return ucwords($display);
}, 10, 3);

add_action('wp', function (): void {
if (!is_singular('page')) {
return;
}

$postId = (int) get_the_ID();
$raw = get_field('service_slug', $postId, false);
$formatted = get_field('service_slug', $postId, true);

error_log('[service-slug] post=' . $postId . ' raw=' . (string) $raw . ' formatted=' . (string) $formatted);
});
terminal: command
wp eval 'update_field("service_slug","Laser Hair Removal",1360); echo get_field("service_slug",1360,false) . PHP_EOL;'
wp eval 'echo get_field("service_slug",1360,true) . PHP_EOL;'
wp post meta get 1360 service_slug
terminal: output
laser-hair-removal
Laser Hair Removal
laser-hair-removal
warning

format_value must stay pure. Any writes belong in update_value or explicit migration scripts.

Common Mistakes

MistakeRoot CauseWhat Breaks in ProductionCorrect Pattern
Normalizing values in format_valueLifecycle misunderstandingCanonical storage drifts unpredictablyNormalize only in update_value
Writing DB inside format_value callbackSide-effect coding styleHidden writes during read operationsKeep format_value read-only and pure
Using unscoped global filters for critical fieldsConvenience over precisionUnintended transformations on unrelated fieldsUse name= or key= targeted filters
No validation before update normalizationInvalid values pass through normalizationCorrupted or misleading canonical dataPair validate_value with update_value
No raw-vs-formatted verification checksTeams assume output equals storageMigration mistakes go unnoticedCompare get_field(..., false) vs get_field(..., true)
Permanent legacy bridges with no sunsetTemporary compatibility never removedLong-term technical debt and ambiguityTrack bridge reports and remove after migration
Deep Dive: Why Mixing update_value and format_value Logic Is Costly

When storage normalization and display formatting are mixed, developers lose a stable canonical source. Query behavior becomes inconsistent, data exports become unreliable, and migration assumptions fail. It also makes debugging harder because a value may appear one way in templates and another way in raw meta without clear policy. Keeping these stages separate gives you one truth for storage and one controlled representation for UI. This separation is foundational for analytics, integrations, and rollback safety.

wp eval '$raw=get_field("service_slug",1360,false); $fmt=get_field("service_slug",1360,true); echo "raw=" . $raw . PHP_EOL; echo "fmt=" . $fmt . PHP_EOL;'

Best Practices

  1. Define canonical storage contract first, then write update_value to enforce it.
  2. Use validate_value to reject malformed inputs before normalization runs.
  3. Keep format_value pure and side-effect free (no writes, no network calls).
  4. Scope filters by field name/key for production-critical transformations.
  5. Add CLI tests that compare raw stored value vs formatted output.
  6. Track legacy load bridges with report options and deprecation timeline.
  7. Document filter ownership (file, target, priority, purpose) for maintainability.

Hands-On Practice

Exercise 1: Add phone validation + normalization filters

Create wp-content/themes/clinic-pro/inc/acf/phone-normalization.php and run:

wp eval '$normalized=apply_filters("acf/update_value/name=sales_phone","(65) 8123-9999",1360,["name"=>"sales_phone"],"(65) 8123-9999"); echo $normalized . PHP_EOL;'

After completing this exercise, output should be:

+6581239999

Exercise 2: Verify canonical storage and formatted output diverge intentionally

Run:

wp eval 'update_field("sales_phone","(65) 8123-9999",1360); echo get_field("sales_phone",1360,false) . PHP_EOL; echo get_field("sales_phone",1360,true) . PHP_EOL;'

After completing this exercise, output should be:

+6581239999
+65 8123 9999

Exercise 3: Implement and test legacy load_value bridge

Create compatibility bridge file and run:

wp eval '$resolved=apply_filters("acf/load_value/name=service_status","ACTIVE",1360,["name"=>"service_status"]); echo $resolved . PHP_EOL;'

After completing this exercise, output should be:

active

Exercise 4: Inspect migration bridge report option

Run:

wp eval 'print_r(get_option("acf_status_load_bridge_report",[]));'

After completing this exercise, output should include at least one compatibility conversion record:

[raw] => ACTIVE
[resolved] => active

Exercise 5: Confirm no mutation in format_value path

Run:

wp eval 'update_field("service_slug","Laser Hair Removal",1360); $raw1=get_field("service_slug",1360,false); $fmt=get_field("service_slug",1360,true); $raw2=get_field("service_slug",1360,false); echo $raw1 . "|" . $fmt . "|" . $raw2 . PHP_EOL;'

After completing this exercise, output should show raw storage unchanged before and after formatting:

laser-hair-removal|Laser Hair Removal|laser-hair-removal

CLI Reference

CommandPurposeReal Example Output
wp eval '$v=apply_filters("acf/update_value/name=sales_phone","(65) 8123-9999",1360,["name"=>"sales_phone"],"(65) 8123-9999"); var_export($v);'Test update-stage normalization result'+6581239999'
wp eval 'update_field("sales_phone","(65) 8123-9999",1360); echo get_field("sales_phone",1360,false) . PHP_EOL;'Verify canonical raw storage+6581239999
wp eval 'echo get_field("sales_phone",1360,true) . PHP_EOL;'Verify format-stage display rendering+65 8123 9999
wp eval '$v=apply_filters("acf/load_value/name=service_status","ACTIVE",1360,["name"=>"service_status"]); var_export($v);'Test load-stage compatibility conversion'active'
wp post meta get 1360 service_slugInspect canonical slug storage after update filterlaser-hair-removal
wp eval 'echo get_field("service_slug",1360,true) . PHP_EOL;'Inspect presentation formatting for same valueLaser Hair Removal
wp eval 'print_r(get_option("acf_status_load_bridge_report",[]));'Inspect compatibility bridge activity logsArray ( [0] => Array ( [raw] => ACTIVE ... ) )
`wp plugin list --status=activegrep advanced-custom-fields-pro`Confirm ACF Pro dependency

What's Next

tip

Revisit this lesson before writing any new ACF value transform; choosing the wrong lifecycle stage is the most expensive avoidable mistake.