Skip to main content

validate_value, Sanitization, and Publish Guards

Strong ACF validation means bad data never reaches storage or publication workflows.

Learning Focus

You will implement field-level and cross-field validation with acf/validate_value, enforce canonical storage with sanitization callbacks, and build publish guards that block risky content states before they reach production.

Concept Overview

ACF provides field-level constraints, but real business rules often span multiple fields and publication state. Examples include start/end date logic, legal disclaimers, compliance ownership requirements, and conditional mandatory fields. These rules belong in server-side hooks, not editorial memory.

Validation and sanitization are related but distinct. Validation decides if input is acceptable. Sanitization converts acceptable input into canonical storage shape. Mixing them can produce ambiguous behavior and hidden data mutations.

Publish guards are policy enforcement points at or after save. They are useful when rule outcomes depend on post status transitions, role, or composite field readiness. Good guardrails provide actionable error feedback and observable logs so content teams can self-correct quickly.

Core Idea

Validate business rules before save, sanitize canonical values on update, and guard publication pathways when compliance conditions are unmet.

Why It Matters

ApproachWhat HappensImpact in Production
Enforce cross-field rules on submitInvalid combinations are blocked earlyHigher data quality and fewer rollback edits
Sanitize canonical values before storageQuery and export behavior remains consistentBetter integration reliability
Apply publish guards for compliance-sensitive fieldsRisky content states are prevented from releaseLower legal/operational incident risk
Return clear, field-targeted messagesEditors fix issues immediatelyLower support load and faster publishing
Rely on manual QA only (wrong pattern)Invalid data leaks under deadline pressureExpensive post-release corrections

Reference Table

Term/APISignature/SyntaxPurposeKey Notes
acf/validate_valueadd_filter('acf/validate_value', callable $callback, int $priority = 10, int $accepted_args = 4): voidReject invalid values pre-saveReturn true or string error message
Name-targeted validationacf/validate_value/name=campaign_end_dateField-specific rule enforcementPreferred for readability
Key-targeted validationacf/validate_value/key=field_campaign_end_dateImmutable target precisionUseful in refactor-heavy projects
acf/update_valueadd_filter('acf/update_value', callable $callback, int $priority = 10, int $accepted_args = 4): voidCanonical sanitization before writeKeep pure and idempotent
acf/save_postadd_action('acf/save_post', callable $callback, int $priority = 10, int $accepted_args = 1): voidPost-save publication guardrailsUse cautiously, avoid recursion
acf_add_validation_error()acf_add_validation_error(string $input, string $message): voidAdd targeted validation feedbackUseful in custom form workflows
wp_update_post()`wp_update_post(array $postarr, bool $wp_error = false): intWP_Error`Adjust post status in publish guard flows
wp evalwp eval '<php-code>'Validate hook behavior and sanitization outputsCore CLI QA path

Hook Targeting Syntax

VariantExampleWhen to Use
Global validateacf/validate_valueRare broad instrumentation
By nameacf/validate_value/name=lead_emailMost custom business rules
By keyacf/validate_value/key=field_lead_emailStrict target safety in large teams
Global updateacf/update_valueShared sanitizer dispatch patterns

Practical Use Cases

Use Case 1 — Block invalid campaign date windows (cross-field validation)

A campaign page cannot be published with campaign_end_date earlier than campaign_start_date. Native single-field constraints are insufficient because this is a cross-field rule.

  1. Target campaign_end_date in validate_value.
  2. Read submitted start date from request payload safely.
  3. Parse both dates and compare timestamps.
  4. Return actionable error when invalid.
  5. Verify rule through CLI filter simulation.
wp-content/themes/clinic-pro/inc/acf/campaign-date-validation.php
<?php

declare(strict_types=1);

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

$startRaw = '';
if (isset($_POST['acf']) && is_array($_POST['acf'])) {
$startRaw = (string) ($_POST['acf']['field_campaign_start_date'] ?? '');
}

$endRaw = (string) $value;
if ($startRaw === '' || $endRaw === '') {
return true;
}

$start = strtotime($startRaw);
$end = strtotime($endRaw);

if ($start === false || $end === false) {
return 'Campaign dates must be valid date values.';
}

if ($end < $start) {
return 'Campaign end date must be later than campaign start date.';
}

return true;
}, 10, 4);

add_filter('acf/update_value/name=campaign_slug', function ($value, $post_id, $field, $original) {
return sanitize_title((string) $value);
}, 10, 4);
terminal: command
wp eval '$_POST["acf"]["field_campaign_start_date"]="2026-03-10"; $valid=apply_filters("acf/validate_value/name=campaign_end_date", true, "2026-03-09", ["name"=>"campaign_end_date"], "acf[field_campaign_end_date]"); var_export($valid); echo PHP_EOL;'
wp eval '$_POST["acf"]["field_campaign_start_date"]="2026-03-10"; $valid=apply_filters("acf/validate_value/name=campaign_end_date", true, "2026-03-20", ["name"=>"campaign_end_date"], "acf[field_campaign_end_date]"); var_export($valid); echo PHP_EOL;'
terminal: output
'Campaign end date must be later than campaign start date.'
true
warning

Cross-field validation should not depend on client-side checks. Always enforce server-side in hooks.

Healthcare content must include legal disclaimer text and a valid policy URL before publication. Drafts can be incomplete, but publish transitions must be guarded.

  1. Validate policy URL format on submit.
  2. Sanitize URL before storage.
  3. On save, detect publish state and required disclaimer presence.
  4. If missing, demote post to draft and record reason.
  5. Expose guard results in option report for release review.
wp-content/themes/clinic-pro/inc/acf/publish-guard-compliance.php
<?php

declare(strict_types=1);

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

$url = trim((string) $value);
if ($url === '') {
return true;
}

if (!filter_var($url, FILTER_VALIDATE_URL)) {
return 'Legal policy URL must be a valid URL.';
}

return true;
}, 10, 4);

add_filter('acf/update_value/name=legal_policy_url', function ($value, $post_id, $field, $original) {
return esc_url_raw((string) $value);
}, 20, 4);

add_action('acf/save_post', function ($post_id): void {
if (!is_numeric($post_id)) {
return;
}

static $guardInProgress = false;
if ($guardInProgress) {
return;
}

$postId = (int) $post_id;
$post = get_post($postId);
if (!$post instanceof WP_Post || $post->post_type !== 'page') {
return;
}

if ($post->post_status !== 'publish') {
return;
}

$disclaimer = trim((string) get_field('legal_disclaimer_text', $postId));
$policyUrl = trim((string) get_field('legal_policy_url', $postId));

if ($disclaimer !== '' && $policyUrl !== '') {
return;
}

$guardInProgress = true;
wp_update_post([
'ID' => $postId,
'post_status' => 'draft',
]);

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

$report[] = [
'post_id' => $postId,
'reason' => 'missing_compliance_fields',
'disclaimer_present' => $disclaimer !== '',
'policy_url_present' => $policyUrl !== '',
'at' => gmdate('c'),
];

update_option('acf_publish_guard_report', array_slice($report, -100), false);
$guardInProgress = false;
}, 30);
terminal: command
wp post create --post_title="Compliance Guard Drill" --post_status=publish --post_type=page
wp eval '$id=(int)get_page_by_title("Compliance Guard Drill", OBJECT, "page")->ID; update_field("legal_disclaimer_text","",$id); update_field("legal_policy_url","",$id); do_action("acf/save_post",$id); $p=get_post($id); echo $p->post_status . PHP_EOL;'
wp eval 'print_r(get_option("acf_publish_guard_report",[]));'
terminal: output
Success: Created post 1380.
draft
Array
(
[0] => Array
(
[post_id] => 1380
[reason] => missing_compliance_fields
[disclaimer_present] =>
[policy_url_present] =>
[at] => 2026-02-23T15:02:11+00:00
)
)
note

Publish guards should be explicit policy controls, not hidden side effects. Always log why a post was blocked.

Use Case 3 — Edge case: sanitization only in template allows bad data in DB

A team sanitizes URLs only during rendering, so invalid values are still stored. Downstream exports and integrations consume bad data. Robust pattern sanitizes at update stage and validates on submit.

❌ Fragile Pattern

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

declare(strict_types=1);

add_action('template_redirect', function (): void {
if (!is_page()) {
return;
}

$url = (string) get_field('legal_policy_url');
echo '<a href="' . esc_url($url) . '">Policy</a>';
});

✅ Robust Pattern

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

declare(strict_types=1);

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

$url = trim((string) $value);
if ($url === '') {
return true;
}

if (!filter_var($url, FILTER_VALIDATE_URL)) {
return 'Please enter a valid legal policy URL (including protocol).';
}

return true;
}, 10, 4);

add_filter('acf/update_value/name=legal_policy_url', function ($value, $post_id, $field, $original) {
return esc_url_raw(trim((string) $value));
}, 20, 4);

add_filter('acf/format_value/name=legal_policy_url', function ($value, $post_id, $field) {
$url = esc_url((string) $value);
return $url;
}, 10, 3);
terminal: command
wp eval '$valid=apply_filters("acf/validate_value/name=legal_policy_url", true, "not-a-url", ["name"=>"legal_policy_url"], "acf[field_legal_url]"); var_export($valid); echo PHP_EOL;'
wp eval '$san=apply_filters("acf/update_value/name=legal_policy_url","https://example.com/policy?x=<script>",1380,["name"=>"legal_policy_url"],"https://example.com/policy?x=<script>"); var_export($san); echo PHP_EOL;'
wp eval 'update_field("legal_policy_url","https://example.com/policy?x=<script>",1380); echo get_field("legal_policy_url",1380,false) . PHP_EOL;'
terminal: output
'Please enter a valid legal policy URL (including protocol).'
'https://example.com/policy?x=%3Cscript%3E'
https://example.com/policy?x=%3Cscript%3E
warning

Template-time escaping protects output, not stored data quality. Canonical sanitization must happen before storage.

Common Mistakes

MistakeRoot CauseWhat Breaks in ProductionCorrect Pattern
Cross-field rules implemented only in frontend JSTrusting client-side validationInvalid payloads saved via API/CLIEnforce in acf/validate_value server-side
Sanitizing only at render timeStorage integrity ignoredExports/integrations consume malformed valuesSanitize in acf/update_value
Generic error messagesValidation UX not designedEditors cannot self-correct quicklyReturn specific actionable error text
Publish guard without recursion protectionStatus updates retrigger hooksRepeated updates and noisy logsUse guard flags and targeted post-type checks
Blocking publish with no audit trailOperational observability gapHard to debug why content remains draftPersist structured guard reports in option/logs
Unscoped validation filtersOver-broad callback behaviorUnexpected failures on unrelated fieldsUse name= or key= hook targeting
Deep Dive: Why Publish Guard Bugs Are Hard to Diagnose

Publish guard logic often changes post status after save, which can trigger additional hooks and obscure original intent. If you do not log guard reason and target fields, teams see "post reverted to draft" without actionable context. This creates confusion between editorial errors and system errors. The fix is to make guard outcomes explicit in structured report entries and include which required fields failed. Then support teams can diagnose in minutes instead of tracing hook stacks manually.

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

Best Practices

  1. Use validate_value for business-rule enforcement and return editor-friendly messages.
  2. Normalize canonical storage in update_value, never in template output code.
  3. Scope validation hooks to field name/key to avoid collateral effects.
  4. Add recursion guards for any publish-state adjustments in acf/save_post.
  5. Persist guard/migration reports for audit and release reviews.
  6. Test validation rules against draft, scheduled, and publish transitions.
  7. Include CLI simulation of invalid and valid payload paths in QA checklist.

Hands-On Practice

Exercise 1: Add cross-field campaign date validation

Create wp-content/themes/clinic-pro/inc/acf/campaign-date-validation.php and run:

wp eval '$_POST["acf"]["field_campaign_start_date"]="2026-03-10"; $v=apply_filters("acf/validate_value/name=campaign_end_date",true,"2026-03-09",["name"=>"campaign_end_date"],"acf[field_campaign_end_date]"); var_export($v); echo PHP_EOL;'

After completing this exercise, output should be:

Campaign end date must be later than campaign start date.

Exercise 2: Implement URL sanitization and verify canonical storage

Run:

wp eval 'update_field("legal_policy_url","https://example.com/policy?x=<script>",1380); echo get_field("legal_policy_url",1380,false) . PHP_EOL;'
wp post meta get 1380 legal_policy_url

After completing this exercise, output should show escaped canonical URL:

https://example.com/policy?x=%3Cscript%3E

Exercise 3: Add publish guard and trigger blocked publish

Run:

wp post create --post_title="Publish Guard Drill" --post_status=publish --post_type=page
wp eval '$id=(int)get_page_by_title("Publish Guard Drill", OBJECT, "page")->ID; update_field("legal_disclaimer_text","",$id); update_field("legal_policy_url","",$id); do_action("acf/save_post",$id); echo get_post($id)->post_status . PHP_EOL;'

After completing this exercise, output should be:

draft

Exercise 4: Inspect guard report payload

Run:

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

After completing this exercise, output should include reason and field presence booleans:

[reason] => missing_compliance_fields
[disclaimer_present] =>
[policy_url_present] =>

Exercise 5: Build release smoke gate for validation/sanitization hooks

Run:

wp eval 'echo "validate_url=" . (int) has_filter("acf/validate_value/name=legal_policy_url") . PHP_EOL; echo "update_url=" . (int) has_filter("acf/update_value/name=legal_policy_url") . PHP_EOL; echo "save_guard=" . (int) has_action("acf/save_post") . PHP_EOL;'

After completing this exercise, output pattern should be:

validate_url=1
update_url=1
save_guard=1

CLI Reference

CommandPurposeReal Example Output
wp eval '$_POST["acf"]["field_campaign_start_date"]="2026-03-10"; $v=apply_filters("acf/validate_value/name=campaign_end_date",true,"2026-03-09",["name"=>"campaign_end_date"],"acf[field_campaign_end_date]"); var_export($v);'Simulate cross-field validation failure'Campaign end date must be later than campaign start date.'
wp eval '$v=apply_filters("acf/validate_value/name=legal_policy_url",true,"not-a-url",["name"=>"legal_policy_url"],"acf[field_legal_url]"); var_export($v);'Test URL validation rule'Legal policy URL must be a valid URL.'
wp eval '$v=apply_filters("acf/update_value/name=legal_policy_url","https://example.com/policy?x=<script>",1380,["name"=>"legal_policy_url"],"https://example.com/policy?x=<script>"); var_export($v);'Test update-stage sanitization'https://example.com/policy?x=%3Cscript%3E'
wp post meta get 1380 legal_policy_urlRead raw canonical stored URLhttps://example.com/policy?x=%3Cscript%3E
wp eval 'do_action("acf/save_post",1380); echo get_post(1380)->post_status . PHP_EOL;'Trigger publish guard and inspect resulting statusdraft
wp eval 'print_r(get_option("acf_publish_guard_report",[]));'Review guard decision reportArray ( [0] => Array ( [reason] => ... ) )
wp eval 'echo has_filter("acf/validate_value/name=campaign_end_date") ? "yes" : "no"; echo PHP_EOL;'Verify hook registrationyes
`wp plugin list --status=activegrep advanced-custom-fields-pro`Confirm dependency state

What's Next

tip

Revisit this lesson before any content model release that touches legal, compliance, or publication workflow fields.