Become the best admin of your WordPress site

Use GraphQL for much more than headless!

  • Modify data in bulk
  • Automate tasks
  • Send notifications
  • Translate posts, block by block
  • Sync content across sites
  • Interact with external services

Handle all your admin tasks and content workflows with Gato GraphQL

Interact with external services


Automate your tasks


Distribute content across a network


Handle large data sets with ease


And much more!

Gato GraphQL is a versatile tool to interact with data.
Input your GraphQL query and deal with your task.

Access the full power of GraphQL at your most convenient

GraphiQL
Query Variables

Use the combined power of REST and GraphQL

Gato GraphQL provides Persisted Queries out of the box. Publish an endpoint under its own URL (like the REST API), but directly from the wp-admin

Augment your capabilities via powerful extensions

Browse the ever-growing catalogue of recipes and customize them to your needs

Augment WordPress search capabilities

Sometimes searching for data within WordPress is limited, as with custom fields: Searching in WordPress for posts that contain some keyword will not search within meta values.

Gato GraphQL can complement these capabilities, allowing us to search for posts (and also users, comments, and taxonomies) by meta key and value (including via regex expressions).

Query dynamic data

Gato GraphQL provides "function" fields, allowing us to compute logic already within the GraphQL query, thus avoiding the need to code a client application.

This way, we can dynamically compute data, input it back into the query, and affect the response with granular control. For instance, we can compute the date "24hs ago" and search for comments added from this date onwards.

Complement WP-CLI

Gato GraphQL can empower WP-CLI, by helping us find the WordPress data we need with granular control, and then injecting into the WP-CLI command (to update a post or user, reply to a comment, delete an option, etc).

We can even retrieve the data from multiple resources at once, and execute WP-CLI on all of them.

For instance, we can select all users with any Spanish locale, and update their locale to Spanish from Argentina.

Send personalized emails

Gato GraphQL allows us to execute mutations under any type from the GraphQL schema (i.e. not only under the Root type).

We can then iterate the list of users, obtain their data (name, email and a meta value with the number of remaining credits), dynamically compose a message using Markdown, and send a personalized email to the user.

Power blocks with DRY logic (for CSR/SSR)

Rendering a dynamic (Gutenberg) block on the client (for the WordPress editor) or on the server-side (for printing the blog post) typically requires fetching the block's data in different ways: using JavaScript to connect to the API endpoint, and using PHP to call WordPress functions, respectively.

Gato GraphQL allows to have a single source of truth to fetch data for both the client and server-sides, making this logic DRY (Don't Repeat Yourself).

Map JavaScript components to blocks

Gato GraphQL provides fields to query the properties from (Gutenberg) blocks.

This allows us to use block data when building a headless (or decoupled) application, mapping each block with a custom JavaScript component.

Duplicate a blog post

Duplicating a post is an example of Gato GraphQL's ability to retrieve, manipulate and store again data in the site.

You have the ultimate control on how the post will be duplicated. Change the author or post status, append "(Copy)" to the title, or any other.

Customize content for different users

We can retrieve a different response in a field depending on some piece of queried data, such as the roles of the logged-in user.

For instance, the post content can append an "Edit this post" link at the bottom of the content for the admin user only.

Adapt content in bulk

Gato GraphQL allows us to execute a mutation on hundreds or even thousands of resources at once.

Migrate the domain, or a post or page slug, in all content

After migrating the site to a new domain, or changing the slug of a post or page, we can convert all content throughout the site to point to the new URL.

Insert/remove a block in bulk

We can update posts by modifying their (Gutenberg) block's HTML content, and we can do it in bulk.

This is useful for promoting campaigns, inserting a custom block with our Call To Action to all posts in the website, and removing it from everywhere right after the campaign ends.

Retrieve structured data from blocks

We can iterate the (Gutenberg) blocks in the post and extract the attributes from deep within the block structure, unlocking these attributes to be fed into any functionality in our site, and to be exposed via an API to power our other applications (mobile app, newsletter, etc).

For instance, by extracting all the image URLs contained in the core/image blocks in a post, we can create an image carousel with all these images.

Automate tasks

Gato GraphQL can help us automate tasks in the application, such as sending a notification email when a comment is added, automatically adding a mandatory block to a new post, pinging external services on some activity, and others.

Gato GraphQL can also be integrated with WP-Cron, allowing us to execute GraphQL queries that run some admin task on a timely basis.

Interact with external services

Gato GraphQL provides fields to execute HTTP requests against a webserver, allowing us to interact with external services and APIs.

For instance, we can retrieve the list of subscribers from a Mailchimp list, combine those records with the user data in the site, and execute an action with the augmented data.

Filter data from an external API

If the external API does not allow filtering by a certain property that we need, we can use Gato GraphQL to iterate over the entries in the API response, and remove those ones that do not satifsy our condition.

query {
postsWithThumbnail: posts( filter: {
metaQuery: {
key: "_thumbnail_id",
compareBy: {
key: { operator: EXISTS }
}
}
} ) {
id
title
featuredImage {
src
}
}

usersWithSpanishLocale: users( filter: {
metaQuery: {
key: "locale",
compareBy: { stringValue: {
operator: REGEXP
value: "es_[A-Z]+"
} }
}
} ) {
id
name
locale: metaValue(key: "locale")
}
}
query {
timeNow: _time
time24HsAgo: _intSubstract(
substract: 86400,
from: $__timeNow
)
date24HsAgo: _date(
format: "Y-m-d\\TH:i:sO",
timestamp: $__time24HsAgo
)
commentsAddedInLast24Hs: comments(
filter: {
dateQuery: {
after: $__date24HsAgo
}
}
) {
date
content
author {
name
}
}
}
GRAPHQL_QUERY='
query RetrieveData {
users( filter: {
metaQuery: {
key: "locale",
compareBy: { stringValue: {
value: "es_[A-Z]+"
operator: REGEXP
} }
}
} ) {
id @export(
as: "userIDs",
type: LIST
)
}
}

query FormatAndPrintData @depends(on: "RetrieveData") {
spanishLocaleUserIDs: _arrayJoin(
array: $userIDs,
separator: " "
)
}
'

GRAPHQL_BODY="{\"operationName\": \"FormatAndPrintData\", \"query\": \"$(echo $GRAPHQL_QUERY | tr '\n' ' ' | sed 's/"/\\"/g')\"}"
GRAPHQL_RESPONSE=$(curl -X POST -H "Content-Type: application/json" -d $GRAPHQL_BODY https://mysite.com/graphql/)
SPANISH_LOCALE_USER_IDS=$(echo $GRAPHQL_RESPONSE | grep -E -o '"spanishLocaleUserIDs\":"((\d|\s)+)"' | cut -d':' -f2- | cut -d'"' -f2- | rev | cut -d'"' -f2- | rev)
for USER_ID in $(echo $SPANISH_LOCALE_USER_IDS); \
do wp user update "$(echo $USER_ID)" --locale=es_AR; \
done
mutation {
users {
email
displayName
remainingCredits: metaValue(key: "credits")

emailMessageTemplate: _strConvertMarkdownToHTML(
text: """
Hello %s,

Your have **%s remaining credits** in your account.

Would you like to [buy more](%s)?
"""

)
emailMessage: _sprintf(
string: $__emailMessageTemplate,
values: [ $__displayName, $__remainingCredits, "https://mysite.com/buy-credits" ]
)

_sendEmail( input: {
to: $__email
subject: "Remaining credits alert"
messageAs: {
html: $__emailMessage
}
} ) {
status
}
}
}
/** JavaScript: Fetch data to render the block on the client (CSR) */
import dryGraphQLQuery from './graphql-documents/fetch-posts-for-my-block.gql';

const response = await fetch(endpoint, {
body: JSON.stringify({
query: dryGraphQLQuery
)
} );
// Do something with the data
// ...

use GatoGraphQL\InternalGraphQLServer\GraphQLServer;

/** PHP: Fetch data to render the block on the server (SSR) */
$block = [
'render_callback' => function(array $attributes, string $content): string {
$dryGraphQLQuery = __DIR__ . '/blocks/my-block/graphql-documents/fetch-posts-for-my-block.gql';
$response = GraphQLServer::executeQueryInFile($dryGraphQLQuery);
$responseContent = json_decode($response->getContent(), true);
if (isset($responseContent["errors"]) {
return __('Oops, executing the query produced errors');
}
$data = $responseContent["data"];
// Do something with the data
// $content = $this->useGraphQLData($content, $data);
return $content;
},
];
register_block_type("namespace/my-block", $block);
<script type="module">
import { h, render } from 'https://esm.sh/preact';
renderPost(1);
async function renderPost(postId) {
const response = await fetch( "https://mysite.com/graphql/", {
body: JSON.stringify( {
query: `{ post(by: { id: ${ postId } }) { blockDataItems } }`
} )
} );
const json = await response.json();
if (json.errors) { return; }
const blocks = json.data.post?.blockDataItems;
const App = h('div', {}, blocks.map(mapBlockToComponent));
render(App, document.body);
}
function mapBlockToComponent(block) {
if (block.name === 'core/heading') return Heading(block);
if (block.name === 'core/paragraph') return Paragraph(block);
if (block.name === 'core/image') return Image(block);
return null;
}
function Heading(props) {
return h('h2', { dangerouslySetInnerHTML: { __html: props.attributes.content } });
}
function Paragraph(props) {
return h('p', { dangerouslySetInnerHTML: { __html: props.attributes.content } });
}
function Image(props) {
return h('img', { src: props.attributes.url });
}
</script>
query GetPostAndExportData($postId: ID!) {
post(by: { id : $postId }) {
author {
id @export(as: "authorID")
}
categories {
id @export(as: "categoryIDs", type: LIST)
}
rawContent @export(as: "rawContent")
rawExcerpt @export(as: "excerpt")
featuredImage {
id @export(as: "featuredImageID")
}
tags {
id @export(as: "tagIDs", type: LIST)
}
rawTitle @strAppend(string: " (Copy)") @export(as: "title")
}
}

mutation DuplicatePost @depends(on: "GetPostAndExportData") {
createPost(input: {
authorBy: { id: $authorID },
categoriesBy: { ids: $categoryIDs },
contentAs: { html: $rawContent },
excerpt: $excerpt
featuredImageBy: { id: $featuredImageID },
tagsBy: { ids: $tagIDs },
title: $title
}) {
status
}
}
query ExportConditionalVariables {
me {
roleNames @remove
isAdminUser: _inArray(value: "administrator", array: $__roleNames)
@export(as: "isAdminUser")
}
}

query RetrieveContentForAdminUser($postId: ID!)
@include(if: $isAdminUser)
{
post(by: { id : $postId }) {
originalContent: content @remove
wpAdminEditURL @remove
content: _sprintf(
string: "%s<p><a href=\"%s\">%s</a></p>",
values: [ $__originalContent, $__wpAdminEditURL, "(Admin only) Edit post" ]
)
}
}

query RetrieveContentForNonAdminUser($postId: ID!)
@skip(if: $isAdminUser)
{
post(by: { id : $postId }) {
content
}
}

query ExecuteAll
@depends(on: ["ExportConditionalVariables", "RetrieveContentForAdminUser", "RetrieveContentForNonAdminUser"]
) {
id @remove
}
query TransformAndExportData($replaceFrom: [String!]!, $replaceTo: [String!]!, $limit: Int! = 5, $offset: Int! = 0) {
posts: posts( pagination: { limit: $limit, offset: $offset } ) {
title
excerpt
@strReplaceMultiple(
search: $replaceFrom
replaceWith: $replaceTo
affectAdditionalFieldsUnderPos: 1
)
@deferredExport(
as: "postInputs"
type: DICTIONARY
affectAdditionalFieldsUnderPos: 1
)
}
}

mutation UpdatePost($limit: Int! = 5, $offset: Int! = 0)
@depends(on: "TransformAndExportData")
{
adaptedPosts: posts( pagination: { limit: $limit, offset: $offset } ) {
id
postInput: _objectProperty(
object: $postInputs,
by: { key: $__id }
) @remove
update(input: $__postInput) {
status
}
}
}
query ExportData($oldPageSlug: String!, $newPageSlug: String!) {
siteURL: optionValue(name: "siteurl")
oldPageURL: _strAppend(after: $__siteURL, append: $oldPageSlug)
@export(as: "oldPageURL")
newPageURL: _strAppend(after: $__siteURL, append: $newPageSlug)
@export(as: "newPageURL")
}

mutation ReplaceOldWithNewURLInPosts
@depends(on: "ExportData")
{
posts( filter: { search: $oldPageURL } ) {
id
rawContent
adaptedRawContent: _strReplace(
search: $oldPageURL
replaceWith: $newPageURL
in: $__rawContent
)
update(input: {
contentAs: { html: $__adaptedRawContent }
}) {
status
post {
title
excerpt
}
}
}
}
mutation InjectBlock($limit: Int! = 5, $offset: Int! = 0) {
posts: posts( pagination: { limit: $limit, offset: $offset } ) {
rawContent
adaptedRawContent: _strRegexReplace(
in: $__rawContent,
searchRegex: "#(<!-- /wp:paragraph -->[\\s\\S]+<!-- /wp:paragraph -->[\\s\\S]+<!-- /wp:paragraph -->)#U",
replaceWith: "$1<!-- mycompany:black-friday-campaign-video -->\n<figure class=\"wp-block-video\"><video controls src=\"https://mysite.com/videos/BlackFriday2023.mp4\"></video></figure>\n<!-- /mycompany:black-friday-campaign-video -->",
limit: 1
)
update(input: { contentAs: { html: $__adaptedRawContent } }) {
status
}
}
}

mutation RemoveBlock {
posts(filter: { search: "\"<!-- /mycompany:black-friday-campaign-video -->\"" } ) {
rawContent
adaptedRawContent: _strRegexReplace(
in: $__rawContent,
searchRegex: "#(<!-- mycompany:black-friday-campaign-video -->[\\s\\S]+<!-- /mycompany:black-friday-campaign-video -->)#",
replaceWith: ""
)
update(input: { contentAs: { html: $__adaptedRawContent } }) {
status
}
}
}
query GetImageBlockImageURLs($postID: ID!) {
post(by: { id: $postID } ) {
coreImageURLs: blockFlattenedDataItems(
filterBy: { include: "core/image" }
)
@underEachArrayItem(
passValueOnwardsAs: "blockDataItem"
)
@applyField(
name: "_objectProperty"
arguments: {
object: $blockDataItem,
by: {
path: "attributes.url"
}
}
setResultInResponse: true
)
@arrayUnique
}
}
use \GatoGraphQL\InternalGraphQLServer\GraphQLServer;

add_action(
'wp_insert_post',
function (int $postID, WP_Post $post) {
// Provide the query to execute
$query = '{ ... }';
$variables = [
'postTitle' => $post->post_title,
'postContent' => $post->post_content,
'postURL' => get_permalink($post->ID),
]
GraphQLServer::executeQuery($query, $variables, 'SendEmail');
},
10,
2
);

wp_schedule_event(
time(),
'daily',
'gatographql__execute_persisted_query',
[
'daily-stats-by-email-number-of-comments',
[ 'to' => ['admin@mysite.com'] ],
'SendDailyStatsByEmailNumberOfComments',
1 // This is the admin user's ID
]
);
query GetDataFromMailchimp {
mailchimpListMembersJSONObject: _sendJSONObjectItemHTTPRequest(input: {
url: "https://us7.api.mailchimp.com/3.0/lists/{LIST_ID}/members",
method: GET,
options: {
auth: {
username: "{ USER }",
password: "{ API_TOKEN }"
}
}
})
@underJSONObjectProperty(by: { key: "members"})
@underEachArrayItem
@underJSONObjectProperty(by: { key: "email_address"})
@export(as: "mailchimpListMemberEmails")
}

query GetUsersUsingMailchimpSubscriberEmails
@depends(on: "GetDataFromMailchimp")
{
users(filter: { searchBy: { emails: $mailchimpListMemberEmails } } ) {
id
name
email
}
}
query {
usersWithWebsiteURL: _sendJSONObjectCollectionHTTPRequest(
input: {
url: "https://some-wp-rest-api.com/wp-json/wp/v2/users/?_fields=id,name,url"
}
)
@underEachArrayItem(
passValueOnwardsAs: "userDataEntry"
affectDirectivesUnderPos: [1, 2, 3]
)
@applyField(
name: "_objectProperty"
arguments: {
object: $userDataEntry
by: {
key: "url"
}
}
passOnwardsAs: "websiteURL"
)
@applyField(
name: "_isEmpty"
arguments: {
value: $websiteURL
}
passOnwardsAs: "isWebsiteURLEmpty"
)
@if(condition: $isWebsiteURLEmpty)
@setNull
@arrayFilter
}

Power your headless application

Plugin screenshots

Private GraphiQL client

Execute your GraphQL queries in the wp-admin via a private GraphiQL client.

Private Interactive Schema client

Interactively browse the private GraphQL schema in the wp-admin, exploring all connections among entities.

Public GraphiQL client

The GraphiQL client for the single endpoint is exposed to the Internet.

Public Interactive Schema client

Interactively browse the public GraphQL schema exposed for the single endpoint.

Persisted Queries

Persisted queries are pre-defined and stored in the server.

Executing Persisted Queries

Requesting a persisted query URL will retrieve its pre-defined GraphQL response.

Custom Endpoints

We can create multiple custom endpoints, each for a different target.

Schema Configurations

All endpoints (the single endpoint, custom endpoints, persisted queries, and internal endpoints for the application) are configured via Schema Configurations.

Schema Configurations

We can create many Schema Configurations, customizing them for different users or applications.

Public or Private Endpoints

Custom endpoints and persisted queries can be public, private and password-protected.

Endpoint Categories

Manage custom endpoints and persisted queries by adding categories to them.

Security

We can configure exactly what custom post types, options and meta keys can be queried on an endpoint by endpoint basis.

Settings

Configure every aspect from the plugin via the Settings page.

Modules

Modules providing different functionalities and GraphQL schema extensions can be enabled and disabled.

Extensions

Augment the plugin functionality and GraphQL schema via extensions.

Recipes

The Recipes section contains example queries ready to copy/paste and use.

Access Control (extension)

Validate who can access the endpoint in granular fashion (down to the operation, field and directive level), based on user roles and capabilities, visitor IP, and more.

Cache Control (extension)

Cache the API response via standard HTTP caching, with the max-age automatically calculated based on the fields present in the query.

Field Deprecation via UI (extension)

Deprecate fields to evolve the GraphQL schema directly from the user interface.

GraphiQL client to execute queries in the wp-admin

Interactively browse the private GraphQL schema, exploring all connections among entities

The GraphiQL client for the single endpoint is exposed to the Internet

Interactively browse the public GraphQL schema exposed for the single endpoint

Persisted queries are pre-defined and stored in the server

Requesting a persisted query URL will retrieve its pre-defined GraphQL response

We can create multiple custom endpoints, each for a different target

Endpoints are configured via Schema Configurations

We can create many Schema Configurations, customizing them for different users or applications

Custom endpoints and Persisted queries can be public, private and password-protected

Manage custom endpoints and persisted queries by adding categories to them

We can configure exactly what custom post types, options and meta keys can be queried on an endpoint by endpoint basis

Configure every aspect from the plugin via the Settings page

Modules providing different functionalities and GraphQL schema extensions can be enabled and disabled

Augment the plugin functionality and GraphQL schema via extensions

The Recipes section contains example queries ready to copy/paste and use

Validate who can access the endpoint in granular fashion (down to the operation, field and directive level), based on user roles and capabilities, visitor IP, and more

Cache the API response via standard HTTP caching, with the max-age automatically calculated based on the fields present in the query

Deprecate fields to evolve the GraphQL schema directly from the user interface