Skip to main content

Relationship, Taxonomy, and User Fields

Relational fields let you reference canonical records instead of duplicating content across templates.

Learning Focus

You will design post, term, and user relationships as stable data contracts, implement safe rendering patterns for different return formats, and validate relational integrity with CLI audits.

Concept Overview

Relational modeling in ACF connects entities instead of copying text. Relationship and Post Object fields connect posts. Taxonomy fields connect terms. User fields connect site users. This shift from duplication to references reduces editorial overhead and makes updates propagate naturally.

The key detail is return format: object vs ID vs array. Many production bugs come from code assuming one shape while field settings return another. That is why relational fields should be modeled with explicit return format decisions and verified through CLI checks after deployment.

In ACF Pro workflows, relational fields often combine with structural fields. For example, a Repeater row can include a Post Object reference, or Flexible layouts can include taxonomy-based filtering controls. These combinations are powerful but require explicit null guards and shape-aware rendering logic.

Core Idea

Prefer canonical references over duplicated content, and always align template logic with the field's configured return format.

Why It Matters

ApproachWhat HappensImpact in Production
Reference related resources via Relationship fieldsOne update to canonical resource propagates everywhereLower maintenance and fewer stale links
Copy-paste related snippets into many pagesData drifts and conflicts over timeHigh editorial rework and inconsistency
Use User field for ownership metadataResponsible person is explicit and renderableBetter accountability and clearer review flows
Use Taxonomy field for controlled classificationContent grouping and filtering become reliableBetter discoverability and reporting
Ignore return format contracts (wrong pattern)Template expects object but receives ID (or inverse)Runtime notices, broken loops, and missing output

Reference Table

Term/APISignature/SyntaxPurposeKey Notes
Relationship field'type' => 'relationship', 'post_type' => ['resource']Select multiple related postsReturn format may be object or ID array
Post Object field'type' => 'post_object', 'post_type' => ['case_study']Select one related postSimpler one-to-one reference than Relationship
Taxonomy field'type' => 'taxonomy', 'taxonomy' => 'industry'Assign or reference termsConfigure return as term ID/object
User field'type' => 'user'Assign user references (owner/reviewer)Configure role constraints where needed
get_field()`get_field(string $selector, intstring $post_id = false, bool $format_value = true): mixed`Retrieve relational values
get_permalink()`get_permalink(intWP_Post $post): stringfalse`
get_term()`get_term(int $term_id, string $taxonomy = ''): WP_TermWP_Errornull`
get_userdata()`get_userdata(int $user_id): WP_Userfalse`Resolve user IDs into user objects
wp evalwp eval '<php-code>'Audit relational payload shapeCore tool for return-format diagnostics

Practical Use Cases

A tutorial platform wants each guide page to show 3–5 related resources selected by editors. Resources are canonical posts managed by a central content team.

  1. Register Relationship field scoped to tutorial posts.
  2. Restrict selectable post type to resource.
  3. Cap max items to 5.
  4. Render links with normalized object handling.
  5. Verify relational payload shape and count via CLI.
wp-content/themes/clinic-pro/inc/acf/related-resources-model.php
<?php

declare(strict_types=1);

add_action('acf/init', function (): void {
acf_add_local_field_group([
'key' => 'group_tutorial_related_resources',
'title' => 'Tutorial Related Resources',
'fields' => [
[
'key' => 'field_related_resources',
'label' => 'Related Resources',
'name' => 'related_resources',
'type' => 'relationship',
'post_type' => ['resource'],
'max' => 5,
'return_format' => 'object',
'filters' => ['search'],
],
],
'location' => [[[
'param' => 'post_type',
'operator' => '==',
'value' => 'tutorial',
]]],
]);
});

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

$resources = get_field('related_resources');
if (!is_array($resources) || count($resources) === 0) {
return;
}

echo '<aside class="related-resources"><ul>';

foreach ($resources as $item) {
$postObject = $item instanceof WP_Post ? $item : get_post((int) $item);
if (!$postObject instanceof WP_Post) {
continue;
}

$url = get_permalink($postObject);
if (!$url) {
continue;
}

echo '<li><a href="' . esc_url($url) . '">' . esc_html(get_the_title($postObject)) . '</a></li>';
}

echo '</ul></aside>';
});
terminal: command
wp post create --post_title="Resource ACF Guide" --post_status=publish --post_type=resource
wp post create --post_title="Resource Schema Checklist" --post_status=publish --post_type=resource
wp post create --post_title="Tutorial Relational Drill" --post_status=publish --post_type=tutorial
wp eval '$tutorial=(int)get_page_by_title("Tutorial Relational Drill", OBJECT, "tutorial")->ID; $res1=(int)get_page_by_title("Resource ACF Guide", OBJECT, "resource")->ID; $res2=(int)get_page_by_title("Resource Schema Checklist", OBJECT, "resource")->ID; update_field("related_resources", [$res1, $res2], $tutorial); $items=get_field("related_resources",$tutorial); echo count((array)$items) . PHP_EOL;'
terminal: output
Success: Created post 1350.
Success: Created post 1351.
Success: Created post 1352.
2
note

Even if return format is object, normalization logic protects you from environment drift and future setting changes.

Use Case 2 — Combine User + Taxonomy fields for accountability and segmentation

A case-study workflow needs two references: the responsible account manager (User field) and the industry segment (Taxonomy field). These values drive header badges and filtering widgets.

  1. Register User field with role constraints.
  2. Register Taxonomy field for industry_segment.
  3. Keep return format explicit (id for both fields in this scenario).
  4. Normalize IDs to objects in render code.
  5. Verify user and term mapping via CLI.
wp-content/themes/clinic-pro/inc/acf/case-ownership-model.php
<?php

declare(strict_types=1);

add_action('acf/init', function (): void {
acf_add_local_field_group([
'key' => 'group_case_ownership_model',
'title' => 'Case Ownership Model',
'fields' => [
[
'key' => 'field_case_account_manager',
'label' => 'Account Manager',
'name' => 'case_account_manager',
'type' => 'user',
'role' => ['editor', 'administrator'],
'return_format' => 'id',
'required' => 1,
],
[
'key' => 'field_case_industry_segment',
'label' => 'Industry Segment',
'name' => 'case_industry_segment',
'type' => 'taxonomy',
'taxonomy' => 'industry_segment',
'field_type' => 'select',
'return_format' => 'id',
'add_term' => 0,
'save_terms' => 1,
],
],
'location' => [[[
'param' => 'post_type',
'operator' => '==',
'value' => 'case_study',
]]],
]);
});

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

$userId = (int) get_field('case_account_manager');
$termId = (int) get_field('case_industry_segment');

$user = $userId > 0 ? get_userdata($userId) : false;
$term = $termId > 0 ? get_term($termId, 'industry_segment') : null;

if ($user instanceof WP_User) {
echo '<span class="owner">' . esc_html($user->display_name) . '</span>';
}

if ($term instanceof WP_Term) {
echo '<span class="industry">' . esc_html($term->name) . '</span>';
}
});
terminal: command
wp term create industry_segment Healthcare --slug=healthcare
wp post create --post_title="Case Relational Drill" --post_status=publish --post_type=case_study
wp eval '$id=(int)get_page_by_title("Case Relational Drill", OBJECT, "case_study")->ID; $user=get_current_user_id(); $term=get_term_by("slug","healthcare","industry_segment"); update_field("case_account_manager", $user, $id); update_field("case_industry_segment", (int)$term->term_id, $id); echo get_field("case_account_manager",$id) . "|" . get_field("case_industry_segment",$id) . PHP_EOL;'
terminal: output
Success: Created industry_segment 31.
Success: Created post 1353.
1|31
warning

When return format is id, always normalize through get_userdata() or get_term() before assuming object properties exist.

Use Case 3 — Edge case: return-format mismatch breaks template assumptions

A developer writes code assuming Relationship returns post objects, but a field setting change switches return format to IDs. Template code now attempts $item->ID on integers and fails.

❌ Fragile Pattern

wp-content/themes/clinic-pro/template-parts/related-fragile.php
<?php

declare(strict_types=1);

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

$resources = get_field('related_resources');
foreach ((array) $resources as $item) {
echo '<a href="' . esc_url(get_permalink($item->ID)) . '">' . esc_html(get_the_title($item->ID)) . '</a>';
}
});

✅ Robust Pattern

wp-content/themes/clinic-pro/template-parts/related-robust.php
<?php

declare(strict_types=1);

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

$resources = get_field('related_resources');
if (!is_array($resources) || count($resources) === 0) {
return;
}

echo '<ul class="related">';

foreach ($resources as $item) {
$postId = 0;

if ($item instanceof WP_Post) {
$postId = (int) $item->ID;
} elseif (is_numeric($item)) {
$postId = (int) $item;
}

if ($postId <= 0) {
continue;
}

$url = get_permalink($postId);
$title = get_the_title($postId);

if (!$url || $title === '') {
continue;
}

echo '<li><a href="' . esc_url($url) . '">' . esc_html($title) . '</a></li>';
}

echo '</ul>';
});
terminal: command
wp eval '$id=(int)get_page_by_title("Tutorial Relational Drill", OBJECT, "tutorial")->ID; $items=get_field("related_resources",$id); echo gettype($items[0]) . PHP_EOL;'
wp eval '$id=(int)get_page_by_title("Tutorial Relational Drill", OBJECT, "tutorial")->ID; $items=(array)get_field("related_resources",$id); foreach($items as $item){if($item instanceof WP_Post){echo "object:" . $item->ID . PHP_EOL;} elseif(is_numeric($item)){echo "id:" . (int)$item . PHP_EOL;}}'
terminal: output
integer
id:1350
id:1351
warning

Return-format drift is common after schema edits. Always write relationship rendering code that can normalize object and ID payloads safely.

Common Mistakes

MistakeRoot CauseWhat Breaks in ProductionCorrect Pattern
Duplicating related content manuallyNo relational model planningStale and conflicting content across pagesUse Relationship/Post Object fields and render canonical records
Assuming relationship return shapeField setting changed from object to IDTemplate property access errors (->ID on int)Normalize payload type and guard with instanceof
Unbounded relationship selectorsNo post_type/status constraintsEditors select irrelevant recordsRestrict Relationship post_type and max
Missing null guards for user/term referencesDeleted users/terms not handledBlank headers or notices in templatesResolve IDs via get_userdata() / get_term() with fallback
Using Post Object where multiple refs neededWrong cardinality choiceWorkarounds and duplicate fields appearUse Relationship for multi-select scenarios
No CLI relational audits in release processDrift remains invisible until runtimeLate-stage production debuggingAdd wp eval checks for counts and payload shapes
Deep Dive: Why Return-Format Mismatch Is Harder to Debug Than It Looks

Return-format mismatch can evade tests because one environment may still have old field settings while another has updated ones. A template that worked yesterday can start failing after a seemingly harmless field setting change. The data itself is still valid, but code assumptions become invalid. This feels random unless you inspect actual runtime payload types directly. Add payload-type checks to your CLI release checklist to catch this early.

wp eval '$id=(int)get_page_by_title("Tutorial Relational Drill", OBJECT, "tutorial")->ID; $items=(array)get_field("related_resources",$id); foreach($items as $item){echo gettype($item) . PHP_EOL;}'

Best Practices

  1. Prefer references over duplication: model canonical records once and link them.
  2. Set cardinality explicitly: Post Object for one-to-one, Relationship for one-to-many.
  3. Choose and document return format in schema notes: object or ID is a code contract.
  4. Normalize payloads before rendering: handle both WP_Post and numeric IDs defensively.
  5. Constrain selectors for editorial clarity: set post_type, max items, and filters.
  6. Guard deleted/missing entities in templates: skip invalid term/user/post references safely.
  7. Audit relational payloads in CLI before releases: verify counts, types, and target IDs.

Hands-On Practice

Create wp-content/themes/clinic-pro/inc/acf/related-resources-model.php and run:

wp eval '$g=acf_get_field_groups(["key"=>"group_tutorial_related_resources"]); echo count($g) . PHP_EOL;'

After completing this exercise, output should be:

1

Exercise 2: Seed tutorial-resource relationships

Run:

wp eval '$tutorial=(int)get_page_by_title("Tutorial Relational Drill", OBJECT, "tutorial")->ID; $a=(int)get_page_by_title("Resource ACF Guide", OBJECT, "resource")->ID; $b=(int)get_page_by_title("Resource Schema Checklist", OBJECT, "resource")->ID; update_field("related_resources", [$a,$b], $tutorial); echo count((array)get_field("related_resources",$tutorial)) . PHP_EOL;'

After completing this exercise, output should be:

2

Exercise 3: Register case ownership model and assign values

Run:

wp eval '$id=(int)get_page_by_title("Case Relational Drill", OBJECT, "case_study")->ID; $user=get_current_user_id(); $term=get_term_by("slug","healthcare","industry_segment"); update_field("case_account_manager",$user,$id); update_field("case_industry_segment",(int)$term->term_id,$id); echo get_field("case_account_manager",$id) . "|" . get_field("case_industry_segment",$id) . PHP_EOL;'

After completing this exercise, output should be:

1|31

Exercise 4: Validate payload types for relationship field

Run:

wp eval '$id=(int)get_page_by_title("Tutorial Relational Drill", OBJECT, "tutorial")->ID; $items=(array)get_field("related_resources",$id); foreach($items as $item){echo gettype($item) . PHP_EOL;}'

After completing this exercise, expected output is either all integer or all object depending on configured return format.

integer
integer

Exercise 5: Verify user and term object resolution safety

Run:

wp eval '$id=(int)get_page_by_title("Case Relational Drill", OBJECT, "case_study")->ID; $u=(int)get_field("case_account_manager",$id); $t=(int)get_field("case_industry_segment",$id); $user=get_userdata($u); $term=get_term($t,"industry_segment"); echo ($user instanceof WP_User ? $user->user_login : "missing-user") . "|" . ($term instanceof WP_Term ? $term->slug : "missing-term") . PHP_EOL;'

After completing this exercise, output should look like:

admin|healthcare

CLI Reference

CommandPurposeReal Example Output
wp eval 'print_r(array_column(acf_get_field_groups(), "key"));'List runtime relational groupsgroup_tutorial_related_resources group_case_ownership_model
wp eval '$id=1352; $items=(array)get_field("related_resources",$id); echo count($items) . PHP_EOL;'Count related resources for one tutorial2
wp eval '$id=1352; $items=(array)get_field("related_resources",$id); foreach($items as $item){echo gettype($item) . PHP_EOL;}'Inspect relationship return shapeinteger
`wp eval '$id=1353; echo get_field("case_account_manager",$id) . "" . get_field("case_industry_segment",$id) . PHP_EOL;'`Inspect raw user/term references
wp post meta get 1352 related_resourcesRead raw stored relationship metaa:2:{...}
wp eval '$id=1353; $u=get_userdata((int)get_field("case_account_manager",$id)); echo $u instanceof WP_User ? $u->display_name : "missing";'Resolve user reference safelyAdministrator
wp eval '$id=1353; $term=get_term((int)get_field("case_industry_segment",$id),"industry_segment"); echo $term instanceof WP_Term ? $term->name : "missing";'Resolve taxonomy reference safelyHealthcare
`wp plugin list --status=activegrep advanced-custom-fields-pro`Confirm ACF Pro dependency for advanced workflows

What's Next

tip

Revisit this lesson when your team starts duplicating snippets across pages; that is a strong signal you need relational fields instead of copied content.