Relationship, Taxonomy, and User Fields
Relational fields let you reference canonical records instead of duplicating content across templates.
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.
Prefer canonical references over duplicated content, and always align template logic with the field's configured return format.
Why It Matters
| Approach | What Happens | Impact in Production |
|---|---|---|
| Reference related resources via Relationship fields | One update to canonical resource propagates everywhere | Lower maintenance and fewer stale links |
| Copy-paste related snippets into many pages | Data drifts and conflicts over time | High editorial rework and inconsistency |
| Use User field for ownership metadata | Responsible person is explicit and renderable | Better accountability and clearer review flows |
| Use Taxonomy field for controlled classification | Content grouping and filtering become reliable | Better 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/API | Signature/Syntax | Purpose | Key Notes |
|---|---|---|---|
| Relationship field | 'type' => 'relationship', 'post_type' => ['resource'] | Select multiple related posts | Return format may be object or ID array |
| Post Object field | 'type' => 'post_object', 'post_type' => ['case_study'] | Select one related post | Simpler one-to-one reference than Relationship |
| Taxonomy field | 'type' => 'taxonomy', 'taxonomy' => 'industry' | Assign or reference terms | Configure 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, int | string $post_id = false, bool $format_value = true): mixed` | Retrieve relational values |
get_permalink() | `get_permalink(int | WP_Post $post): string | false` |
get_term() | `get_term(int $term_id, string $taxonomy = ''): WP_Term | WP_Error | null` |
get_userdata() | `get_userdata(int $user_id): WP_User | false` | Resolve user IDs into user objects |
wp eval | wp eval '<php-code>' | Audit relational payload shape | Core tool for return-format diagnostics |
Practical Use Cases
Use Case 1 — Build a related resources sidebar with Relationship field
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.
- Register Relationship field scoped to tutorial posts.
- Restrict selectable post type to
resource. - Cap max items to 5.
- Render links with normalized object handling.
- Verify relational payload shape and count via CLI.
<?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>';
});
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;'
Success: Created post 1350.
Success: Created post 1351.
Success: Created post 1352.
2
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.
- Register User field with role constraints.
- Register Taxonomy field for
industry_segment. - Keep return format explicit (
idfor both fields in this scenario). - Normalize IDs to objects in render code.
- Verify user and term mapping via CLI.
<?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>';
}
});
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;'
Success: Created industry_segment 31.
Success: Created post 1353.
1|31
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
<?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
<?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>';
});
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;}}'
integer
id:1350
id:1351
Return-format drift is common after schema edits. Always write relationship rendering code that can normalize object and ID payloads safely.
Common Mistakes
| Mistake | Root Cause | What Breaks in Production | Correct Pattern |
|---|---|---|---|
| Duplicating related content manually | No relational model planning | Stale and conflicting content across pages | Use Relationship/Post Object fields and render canonical records |
| Assuming relationship return shape | Field setting changed from object to ID | Template property access errors (->ID on int) | Normalize payload type and guard with instanceof |
| Unbounded relationship selectors | No post_type/status constraints | Editors select irrelevant records | Restrict Relationship post_type and max |
| Missing null guards for user/term references | Deleted users/terms not handled | Blank headers or notices in templates | Resolve IDs via get_userdata() / get_term() with fallback |
| Using Post Object where multiple refs needed | Wrong cardinality choice | Workarounds and duplicate fields appear | Use Relationship for multi-select scenarios |
| No CLI relational audits in release process | Drift remains invisible until runtime | Late-stage production debugging | Add 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
- Prefer references over duplication: model canonical records once and link them.
- Set cardinality explicitly: Post Object for one-to-one, Relationship for one-to-many.
- Choose and document return format in schema notes: object or ID is a code contract.
- Normalize payloads before rendering: handle both
WP_Postand numeric IDs defensively. - Constrain selectors for editorial clarity: set
post_type, max items, and filters. - Guard deleted/missing entities in templates: skip invalid term/user/post references safely.
- Audit relational payloads in CLI before releases: verify counts, types, and target IDs.
Hands-On Practice
Exercise 1: Register and verify related_resources
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
| Command | Purpose | Real Example Output |
|---|---|---|
wp eval 'print_r(array_column(acf_get_field_groups(), "key"));' | List runtime relational groups | group_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 tutorial | 2 |
wp eval '$id=1352; $items=(array)get_field("related_resources",$id); foreach($items as $item){echo gettype($item) . PHP_EOL;}' | Inspect relationship return shape | integer |
| `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_resources | Read raw stored relationship meta | a: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 safely | Administrator |
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 safely | Healthcare |
| `wp plugin list --status=active | grep advanced-custom-fields-pro` | Confirm ACF Pro dependency for advanced workflows |
What's Next
- Continue to Reading Field Values in Templates.
- Return to Module 2 Overview to revisit modeling decisions with your team.
- Related lesson: Repeater, Group, Clone, and Flexible Content Patterns.
- Related lesson: Field Retrieval APIs and Context-Aware Data Access.
Revisit this lesson when your team starts duplicating snippets across pages; that is a strong signal you need relational fields instead of copied content.