Creating Relationships Between Users and Custom Post Types in WordPress GraphQL

September 21, 2025 by Missie Dawes

The WPGraphQL plugin makes it easy to expose your WordPress content and user data through a GraphQL API. But what if you want to relate existing WordPress objects (like Users) to your own custom objects (like a Team Member post type)?

In this post, I’ll walk through two approaches to creating that relationship:

  1. Code-First Approach – manually define your GraphQL object type and relationship fields.
  2. Schema-Driven Approach – leverage WPGraphQL’s built-in schema generation for custom post types.

Both approaches let you add a teamMember field to the User type, so you can query the linked Team Member post. The difference comes down to how much control you want over the schema.


Step 1: Register the Team Member Post Type

Here’s a minimal CPT registration. Both approaches start here:

add_action('init', function() {
register_post_type('team_member', [
'labels' => ['name' => 'Team Members', 'singular_name' => 'Team Member'],
'public' => true,
'has_archive' => true,
'show_in_rest' => true,
'supports' => ['title', 'editor', 'thumbnail', 'custom-fields'],
'menu_icon' => 'dashicons-groups',
'rewrite' => ['slug' => 'team-member'],
]);
});

Step 2: Add Custom Fields to User Profiles

We’ll leverage user meta to extend User profiles so admins can link a User to a Team Member post:

function codekaizen_save_additional_team_member_fields($user_id) {
if (!current_user_can('edit_user', $user_id)) return false;
update_user_meta($user_id, 'team_member_post_id', intval($_POST['team_member_post_id']));
}
add_action('personal_options_update', 'codekaizen_save_additional_team_member_fields');
add_action('edit_user_profile_update', 'codekaizen_save_additional_team_member_fields');

Now the link between a User and a Team Member post exists in the database. Next, let’s expose it to GraphQL.


Step 3: Define the Relationship

Approach 1: Code-First Approach

In the code-first approach, you define your GraphQL object type (TeamMemberPost) and fields by hand:

add_action('graphql_register_types', function () {
register_graphql_field('User', 'team_member_post_id', [
'type' => 'Int',
'description' => 'Team Member Post',
'resolve' => function ($user) {
$post_id = intval(get_user_meta($user->userId, 'team_member_post_id', true));
return $post_id;
}
]);
register_graphql_object_type('TeamMemberPost', [
'description' => 'Team Member Post object',
'fields' => [
'id' => [
'type' => 'ID',
'description' => 'The ID of the Team Member post',
'resolve' => function ($post) {
return $post->ID;
}
],
'title' => [
'type' => 'String',
'description' => 'The title of the Team Member post',
'resolve' => function ($post) {
return get_the_title($post->ID);
}
],
'content' => [
'type' => 'String',
'description' => 'The content of the Team Member post',
'resolve' => function ($post) {
return apply_filters('the_content', $post->post_content);
}
],
'featuredImage' => [
'type' => 'MediaItem',
'description' => 'The featured image of the Team Member post',
'resolve' => function ($post) {
return get_the_post_thumbnail_url($post->ID, 'full') ? (object)['sourceUrl' => get_the_post_thumbnail_url($post->ID, 'full')] : null;
}
],
],
]);
register_graphql_field('User', 'team_member_post', [
'type' => 'TeamMemberPost',
'description' => 'The Team Member post associated with this user',
'resolve' => function ($user) {
$post_id = intval(get_user_meta($user->userId, 'team_member_post_id', true));
if ($post_id) {
$post = get_post($post_id);
if ($post && $post->post_type === 'team_member') {
return $post;
}
}
return null;
}
]);
});

When to Use This

  • You need total control over the schema.
  • Your object doesn’t map neatly to a CPT.
  • You want to limit fields or shape the API for a very specific client.

Approach 2: Schema-Driven Approach

If your object is a CPT, WPGraphQL can generate the schema for you. Just add:

'show_in_graphql'     => true,
'graphql_single_name' => 'TeamMember',
'graphql_plural_name' => 'TeamMembers',

to your CPT registration. Then add a resolver that returns the correct post:

add_action('graphql_register_types', function () {
register_graphql_field('User', 'team_member_post_id', [
'type' => 'Int',
'description' => 'Team Member Post',
'resolve' => function ($user) {
return intval(get_user_meta($user->userId, 'team_member_post_id', true));
}
]);
register_graphql_field('User', 'team_member', [
'type' => 'TeamMember',
'description' => 'Associated Team Member post',
'resolve' => function ($user) {
$post_id = intval(get_user_meta($user->userId, 'team_member_post_id', true));
if ($post_id) {
$post = get_post($post_id);
return new WPGraphQL\Model\Post($post);
}
return null;
}
]);
});

When to Use This

  • You’re happy with WPGraphQL’s defaults.
  • You want less boilerplate and automatic schema fields.
  • You don’t need a custom object type.

Step 4: Querying the Relationship

Once set up, you can run:

query {
user(id: 1, idType: DATABASE_ID) {
databaseId
teamMember {
id
title
content
}
}
}

Final Thoughts

  • Use the Code-First Approach if you want a handcrafted schema with full control.
  • Use the Schema-Driven Approach if you want to let WPGraphQL do the heavy lifting for CPTs.

With these tools, you can model almost any relationship between WordPress objects and expose them cleanly in GraphQL for your frontend apps.